├── spec ├── dummy │ ├── log │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── javascripts │ │ │ │ └── application.js.erb │ │ ├── views │ │ │ ├── pages │ │ │ │ ├── custom.html.erb │ │ │ │ ├── bootstrap.html.erb │ │ │ │ └── foundation.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ ├── models │ │ │ └── user.rb │ │ └── controllers │ │ │ ├── pages_controller.rb │ │ │ └── application_controller.rb │ ├── config │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── routes.rb │ │ ├── database.yml │ │ ├── secrets.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ └── test.rb │ │ └── application.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ └── rails │ ├── config.ru │ ├── db │ │ ├── migrate │ │ │ ├── 20141004231338_create_users.rb │ │ │ ├── 20200531150818_add_category_to_starburst_announcements.starburst.rb │ │ │ ├── 20200531150819_add_index_and_uniqueness_to_announcement_views.starburst.rb │ │ │ └── 20200531150817_create_announcement_tables.starburst.rb │ │ └── schema.rb │ ├── Rakefile │ └── README.rdoc ├── factories │ ├── user.rb │ ├── announcement_view.rb │ └── announcement.rb ├── support │ ├── capybara.rb │ ├── factory_bot.rb │ ├── webdrivers.rb │ ├── shoulda_matchers.rb │ └── database_cleaner.rb ├── models │ └── starburst │ │ ├── announcement_view_spec.rb │ │ └── announcement_spec.rb ├── controllers │ └── starburst │ │ └── announcements_controller_spec.rb ├── features │ └── announcements_spec.rb ├── lib │ └── starburst_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── .rspec ├── app ├── assets │ ├── images │ │ └── starburst │ │ │ └── .keep │ ├── javascripts │ │ └── starburst │ │ │ ├── starburst.js │ │ │ └── application.js │ └── stylesheets │ │ └── starburst │ │ └── application.css ├── models │ └── starburst │ │ ├── announcement_view.rb │ │ └── announcement.rb ├── views │ ├── announcements │ │ └── starburst │ │ │ ├── _announcement.html.erb │ │ │ ├── _announcement_foundation.html.erb │ │ │ └── _announcement_bootstrap.html.erb │ └── layouts │ │ └── starburst │ │ └── application.html.erb ├── helpers │ └── starburst │ │ └── announcements_helper.rb └── controllers │ └── starburst │ └── announcements_controller.rb ├── .codeclimate.yml ├── lib ├── starburst │ ├── version.rb │ └── engine.rb └── starburst.rb ├── .simplecov ├── config └── routes.rb ├── gemfiles ├── rails_6.0.0.gemfile ├── rails_6.1.0.gemfile ├── rails_5.1.0.gemfile ├── rails_5.2.0.gemfile ├── rails_4.2.0.gemfile └── rails_5.0.0.gemfile ├── Rakefile ├── db └── migrate │ ├── 20141112140703_add_category_to_starburst_announcements.rb │ ├── 20141209221904_add_index_and_uniqueness_to_announcement_views.rb │ └── 20141004214002_create_announcement_tables.rb ├── .gitignore ├── bin └── rails ├── Gemfile ├── Appraisals ├── CHANGELOG.md ├── LICENSE.txt ├── .rubocop.yml ├── .travis.yml ├── starburst.gemspec ├── CODE_OF_CONDUCT.md └── README.md /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require rails_helper 2 | -------------------------------------------------------------------------------- /app/assets/images/starburst/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../javascripts .js 2 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | plugins: 4 | rubocop: 5 | enabled: true 6 | channel: rubocop-0-83 7 | -------------------------------------------------------------------------------- /lib/starburst/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Starburst 4 | VERSION = '2.0.0.rc0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/views/pages/custom.html.erb: -------------------------------------------------------------------------------- 1 |

Custom Announcement

2 | <%= render partial: 'announcements/starburst/announcement' %> 3 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SimpleCov.start :rails do 4 | add_filter File.join('lib', 'starburst', 'version.rb') 5 | end 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Starburst::Engine.routes.draw do 2 | post "announcements/:id/mark_as_read", to: "announcements#mark_as_read", as: "mark_as_read" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/pages/bootstrap.html.erb: -------------------------------------------------------------------------------- 1 |

Bootstrap Announcement

2 | <%= render partial: 'announcements/starburst/announcement_bootstrap' %> 3 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | subscription { '' } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/rails' 4 | require 'capybara/rspec' 5 | 6 | Capybara.server = :webrick 7 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/views/pages/foundation.html.erb: -------------------------------------------------------------------------------- 1 |

Foundation Announcement

2 | <%= render partial: 'announcements/starburst/announcement_foundation' %> 3 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ActiveRecord::Base 4 | def free? 5 | subscription.blank? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require_relative '../config/boot' 6 | require 'rake' 7 | Rake.application.run 8 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Set up gems listed in the Gemfile. 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /lib/starburst/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Starburst 4 | class Engine < ::Rails::Engine 5 | isolate_namespace Starburst 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/announcement_view.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :announcement_view, class: 'Starburst::AnnouncementView' 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /spec/support/webdrivers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Fixes Webdrivers gem warning for older versions. 4 | Webdrivers.cache_time = 10.minutes if Webdrivers.cache_time.zero? 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 6 | load Gem.bin_path('bundler', 'bundle') 7 | -------------------------------------------------------------------------------- /spec/factories/announcement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :announcement, class: 'Starburst::Announcement' do 5 | body { 'Announcement text' } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | APP_PATH = File.expand_path('../config/application', __dir__) 6 | require_relative '../config/boot' 7 | require 'rails/commands' 8 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require File.expand_path('config/environment', __dir__) 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Shoulda::Matchers.configure do |config| 4 | config.integrate do |with| 5 | with.test_framework :rspec 6 | with.library :rails 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | def bootstrap; end 5 | 6 | def custom; end 7 | 8 | def foundation; end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20141112140703_add_category_to_starburst_announcements.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCategoryToStarburstAnnouncements < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :starburst_announcements, :category, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js.erb: -------------------------------------------------------------------------------- 1 | <% if Rails.version >= '5.1.0' %> 2 | <%= require_asset 'rails-ujs' %> 3 | <% else %> 4 | <%= require_asset 'jquery' %> 5 | <%= require_asset 'jquery_ujs' %> 6 | <% end %> 7 | <%= require_asset 'starburst/starburst' %> 8 | -------------------------------------------------------------------------------- /gemfiles/rails_5.1.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.1.0" 6 | gem "rspec-rails", "~> 3.9.0" 7 | gem "shoulda-matchers", "~> 3.1.0" 8 | gem "webdrivers", "~> 3.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.2.0" 6 | gem "rspec-rails", "~> 3.9.0" 7 | gem "shoulda-matchers", "~> 3.1.0" 8 | gem "webdrivers", "~> 3.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20141004231338_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUsers < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :users do |t| 6 | t.string :subscription 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Starburst Announcement Example App 5 | <%= csrf_meta_tags %> 6 | <%= javascript_include_tag 'application' %> 7 | 8 | 9 | <%= yield %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount Starburst::Engine, at: :starburst 5 | 6 | get :bootstrap, to: 'pages#bootstrap' 7 | get :custom, to: 'pages#custom' 8 | get :foundation, to: 'pages#foundation' 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path('config/application', __dir__) 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /app/models/starburst/announcement_view.rb: -------------------------------------------------------------------------------- 1 | module Starburst 2 | class AnnouncementView < ActiveRecord::Base 3 | belongs_to :announcement 4 | belongs_to :user 5 | 6 | validates :announcement_id, presence: true 7 | validates :user_id, presence: true 8 | validates_uniqueness_of :user_id, scope: :announcement_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/announcements/starburst/_announcement.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_announcement %> 2 |
3 |

<%= current_announcement.body.html_safe %>

4 | <%= link_to "Close", starburst.mark_as_read_path(current_announcement.id), :method => :post, :remote => true, :id => "starburst-close" %> 5 |
6 | <% end %> -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | coverage/ 3 | log/*.log 4 | pkg/ 5 | 6 | spec/examples.txt 7 | spec/dummy/db/*.sqlite3 8 | spec/dummy/db/*.sqlite3-journal 9 | spec/dummy/log/*.log 10 | spec/dummy/tmp/ 11 | spec/dummy/.sass-cache 12 | 13 | .byebug_history 14 | .ruby-gemset 15 | .ruby-version 16 | 17 | *.gem 18 | gemfiles/*.lock 19 | Gemfile.lock 20 | -------------------------------------------------------------------------------- /gemfiles/rails_4.2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "jquery-rails", "~> 4.4.0" 6 | gem "rails", "~> 4.2.0" 7 | gem "rspec-rails", "~> 3.9.0" 8 | gem "shoulda-matchers", "~> 3.1.0" 9 | gem "sqlite3", "~> 1.3.9" 10 | gem "webdrivers", "~> 3.0" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails_5.0.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "jquery-rails", "~> 4.4.0" 6 | gem "rails", "~> 5.0.0" 7 | gem "rspec-rails", "~> 3.9.0" 8 | gem "shoulda-matchers", "~> 3.1.0" 9 | gem "sqlite3", "~> 1.3.9" 10 | gem "webdrivers", "~> 3.0" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /app/views/layouts/starburst/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Starburst 5 | <%= stylesheet_link_tag "starburst/application", media: "all" %> 6 | <%= javascript_include_tag "starburst/application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200531150818_add_category_to_starburst_announcements.starburst.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This migration comes from starburst (originally 20141112140703) 3 | 4 | class AddCategoryToStarburstAnnouncements < ActiveRecord::Migration[4.2] 5 | def change 6 | add_column :starburst_announcements, :category, :text 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/starburst/announcements_helper.rb: -------------------------------------------------------------------------------- 1 | module Starburst 2 | module AnnouncementsHelper 3 | 4 | def current_announcement 5 | if (defined? Starburst.current_user_method) && send(Starburst.current_user_method) 6 | @current_announcement ||= Announcement.current(send(Starburst.current_user_method)) 7 | else 8 | false 9 | end 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/announcements/starburst/_announcement_foundation.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_announcement %> 2 |
3 |

<%= current_announcement.body.html_safe %>

4 | <%= link_to "×".html_safe, starburst.mark_as_read_path(current_announcement.id), method: :post, remote: true, class: 'close', id: 'starburst-close' %> 5 |
6 | <% end %> 7 | -------------------------------------------------------------------------------- /db/migrate/20141209221904_add_index_and_uniqueness_to_announcement_views.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIndexAndUniquenessToAnnouncementViews < ActiveRecord::Migration[4.2] 4 | def change 5 | add_index( 6 | :starburst_announcement_views, 7 | %i[user_id announcement_id], 8 | unique: true, 9 | name: 'starburst_announcement_view_index' 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/starburst.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'starburst/engine' 4 | 5 | module Starburst 6 | mattr_accessor :base_controller do 7 | '::ApplicationController' 8 | end 9 | 10 | mattr_accessor :current_user_method do 11 | :current_user 12 | end 13 | 14 | mattr_accessor :user_instance_methods do 15 | [] 16 | end 17 | 18 | def self.configuration 19 | yield self 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/announcements/starburst/_announcement_bootstrap.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_announcement %> 2 | <%= form_tag starburst.mark_as_read_path(current_announcement.id), :remote => true do %> 3 | 7 | <% end %> 8 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200531150819_add_index_and_uniqueness_to_announcement_views.starburst.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This migration comes from starburst (originally 20141209221904) 3 | 4 | class AddIndexAndUniquenessToAnnouncementViews < ActiveRecord::Migration[4.2] 5 | def change 6 | add_index( 7 | :starburst_announcement_views, 8 | %i[user_id announcement_id], 9 | unique: true, 10 | name: 'starburst_announcement_view_index' 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | # Prevent CSRF attacks by raising an exception. 5 | # For APIs, you may want to use :null_session instead. 6 | protect_from_forgery with: :exception 7 | 8 | helper Starburst::AnnouncementsHelper 9 | helper_method :current_user 10 | 11 | def current_user 12 | @_current_user ||= User.find(params[:user_id]) if params[:user_id].present? 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/starburst/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /spec/models/starburst/announcement_view_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Starburst::AnnouncementView do 4 | describe 'relationships' do 5 | it { is_expected.to belong_to(:announcement) } 6 | it { is_expected.to belong_to(:user) } 7 | end 8 | 9 | describe 'validations' do 10 | it { is_expected.to validate_presence_of(:announcement_id) } 11 | it { is_expected.to validate_presence_of(:user_id) } 12 | it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:announcement_id) } # TODO: invert this 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /db/migrate/20141004214002_create_announcement_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateAnnouncementTables < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :starburst_announcement_views do |t| 6 | t.integer :user_id 7 | t.integer :announcement_id 8 | t.timestamps 9 | end 10 | create_table :starburst_announcements do |t| 11 | t.text :title 12 | t.text :body 13 | t.datetime :start_delivering_at 14 | t.datetime :stop_delivering_at 15 | t.text :limit_to_users 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Declare your gem's dependencies in starburst.gemspec. 6 | # Bundler will treat runtime dependencies like base dependencies, and 7 | # development dependencies will be added by default to the :development group. 8 | gemspec 9 | 10 | # Declare any dependencies that are still in development here instead of in 11 | # your gemspec. These might include edge Rails or gems from your path or 12 | # Git. Remember to move these dependencies to your gemspec before releasing 13 | # your gem to rubygems.org. 14 | 15 | # To use debugger 16 | # gem 'debugger' 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/starburst/starburst.js: -------------------------------------------------------------------------------- 1 | function ready(fn) { 2 | if (document.addEventListener) { 3 | document.addEventListener('DOMContentLoaded', fn); 4 | } else { 5 | document.attachEvent('onreadystatechange', function() { 6 | if (document.readyState === 'interactive') 7 | fn(); 8 | }); 9 | } 10 | } 11 | 12 | ready(function addOneTimeMessagesCallbacks() { 13 | var closeButton = document.getElementById('starburst-close'); 14 | if (closeButton !== null) { 15 | closeButton.addEventListener('click', function() { 16 | document.getElementById('starburst-announcement').style.display = 'none'; 17 | }); 18 | } 19 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/starburst/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200531150817_create_announcement_tables.starburst.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This migration comes from starburst (originally 20141004214002) 3 | 4 | class CreateAnnouncementTables < ActiveRecord::Migration[4.2] 5 | def change 6 | create_table :starburst_announcement_views do |t| 7 | t.integer :user_id 8 | t.integer :announcement_id 9 | t.timestamps 10 | end 11 | create_table :starburst_announcements do |t| 12 | t.text :title 13 | t.text :body 14 | t.datetime :start_delivering_at 15 | t.datetime :stop_delivering_at 16 | t.text :limit_to_users 17 | t.timestamps 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.use_transactional_fixtures = false 5 | 6 | config.before(:suite) do 7 | DatabaseCleaner.clean_with(:truncation) 8 | end 9 | 10 | config.before(:each) do # rubocop:disable RSpec/HookArgument 11 | DatabaseCleaner.strategy = :transaction 12 | end 13 | 14 | config.before(:each, type: :feature) do 15 | DatabaseCleaner.strategy = :truncation 16 | end 17 | 18 | config.before(:each) do # rubocop:disable RSpec/HookArgument 19 | DatabaseCleaner.start 20 | end 21 | 22 | config.append_after(:each) do # rubocop:disable RSpec/HookArgument 23 | DatabaseCleaner.clean 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/starburst/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[6.0.0 6.1.0].each do |rails_version| 4 | appraise "rails_#{rails_version}" do 5 | gem 'rails', "~> #{rails_version}" 6 | end 7 | end 8 | 9 | %w[5.2.0 5.1.0].each do |rails_version| 10 | appraise "rails_#{rails_version}" do 11 | gem 'rails', "~> #{rails_version}" 12 | gem 'rspec-rails', '~> 3.9.0' 13 | gem 'shoulda-matchers', '~> 3.1.0' 14 | gem 'webdrivers', '~> 3.0' 15 | end 16 | end 17 | 18 | %w[5.0.0 4.2.0].each do |rails_version| 19 | appraise "rails_#{rails_version}" do 20 | gem 'jquery-rails', '~> 4.4.0' 21 | gem 'rails', "~> #{rails_version}" 22 | gem 'rspec-rails', '~> 3.9.0' 23 | gem 'shoulda-matchers', '~> 3.1.0' 24 | gem 'sqlite3', '~> 1.3.9' 25 | gem 'webdrivers', '~> 3.0' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/starburst/announcements_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Starburst 4 | class AnnouncementsController < Starburst.base_controller.constantize 5 | def mark_as_read 6 | announcement = Announcement.find(params[:id].to_i) 7 | if respond_to?(Starburst.current_user_method, true) && send(Starburst.current_user_method) && announcement 8 | if AnnouncementView.where(user_id: send(Starburst.current_user_method).id, announcement_id: announcement.id).first_or_create(user_id: send(Starburst.current_user_method).id, announcement_id: announcement.id) 9 | render :json => :ok 10 | else 11 | render json: nil, :status => :unprocessable_entity 12 | end 13 | else 14 | render json: nil, :status => :unprocessable_entity 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 0d304ed5a04c85dd2ee79baea5e893fbcfbba9b438bb88faec868977b629f2cf915a0948b509f7d7a24c0c73f805e484939af553543ebe07fbd8387936fe9e28 15 | 16 | test: 17 | secret_key_base: 0ac07cc14cc7f7cc8073e6c164e0d2c7af410229912a72e05f6e8bb9b00dac9aa33543cf3927d339e66685e2bd776337e8dd69151b425ecec5f0d6ee30a16459 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports and disable caching. 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Print deprecation notices to the Rails logger. 19 | config.active_support.deprecation = :log 20 | 21 | # Raise an error on page load if there are pending migrations. 22 | config.active_record.migration_error = :page_load 23 | 24 | # Raises error for missing translations 25 | # config.action_view.raise_on_missing_translations = true 26 | end 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.0.0 (Unreleased) 2 | 3 | ### Bug fixes 4 | * Announcements rendered with the Foundation partial should be processed correctly when closed. 5 | 6 | #### Breaking changes 7 | * Drops support for Rails versions 3.0, 3.1, 3.2, 4.0 and 4.1. 8 | * Raises an error if `Starburst::Announcement.current` is called with a `nil` argument. 9 | 10 | #### Improvements 11 | * Adds support for Rails versions 4.2, 5.0, 5.1, 5.2 and 6.0. 12 | * Removed `Configuration` module. 13 | * `Starburst.current_user_method` now defaults to a symbol value. 14 | * `Starburst.user_instance_methods` now defaults to an empty array. 15 | 16 | #### New features 17 | * Adds support for changing the base controller which `Starburst::AnnouncementsController` inherits from. 18 | * Include private and protected methods when searching for `Starburst.current_user_method`. 19 | 20 | ### 1.0.3 21 | 22 | * Fixes the "mark as read" issue. 23 | 24 | ### 1.0.2 25 | 26 | * Adds support for methods other than `current_user`. 27 | 28 | ### 1.0.1 29 | 30 | * Fixes critical issue where announcements past the first cannot be marked as read. No breaking changes. 31 | 32 | ### 1.0.0 33 | 34 | * First release. 35 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails' 6 | require 'active_record/railtie' 7 | require 'action_controller/railtie' 8 | require 'action_view/railtie' 9 | 10 | Bundler.require(:default, :development) 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | config.load_defaults Rails.gem_version.segments.first(2).join('.') if Rails.version >= '5.1.0' 15 | 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 Corey Martin 4 | Copyright (c) 2019-2020 Starburst Development Team 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | Exclude: 6 | - 'gemfiles/*.gemfile' 7 | - 'spec/dummy/db/schema.rb' 8 | NewCops: enable 9 | 10 | Layout/ArgumentAlignment: 11 | EnforcedStyle: with_fixed_indentation 12 | 13 | Layout/EmptyLineAfterMagicComment: 14 | Exclude: 15 | - 'spec/dummy/db/migrate/*.rb' 16 | 17 | Layout/HashAlignment: 18 | EnforcedHashRocketStyle: table 19 | 20 | Layout/LineLength: 21 | Max: 120 22 | 23 | Layout/MultilineMethodCallIndentation: 24 | EnforcedStyle: indented 25 | 26 | Layout/ParameterAlignment: 27 | EnforcedStyle: with_fixed_indentation 28 | 29 | Metrics/BlockLength: 30 | Exclude: 31 | - 'Rakefile' 32 | - '*.gemspec' 33 | - '**/*.rake' 34 | - 'spec/factories/**/*.rb' 35 | - 'spec/**/*_spec.rb' 36 | 37 | Metrics/MethodLength: 38 | Exclude: 39 | - 'db/migrate/*.rb' 40 | - 'spec/dummy/db/migrate/*.rb' 41 | 42 | Rails: 43 | Enabled: true 44 | 45 | RSpec/DescribeClass: 46 | Exclude: 47 | - 'spec/features/**/*_spec.rb' 48 | 49 | RSpec/LetSetup: 50 | Enabled: false 51 | 52 | RSpec/MultipleExpectations: 53 | Exclude: 54 | - 'spec/features/**/*_spec.rb' 55 | 56 | RSpec/NestedGroups: 57 | Max: 5 58 | 59 | Style/Documentation: 60 | Exclude: 61 | - 'db/migrate/*.rb' 62 | - 'spec/dummy/**/*.rb' 63 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_05_31_150819) do 14 | 15 | create_table "starburst_announcement_views", force: :cascade do |t| 16 | t.integer "user_id" 17 | t.integer "announcement_id" 18 | t.datetime "created_at" 19 | t.datetime "updated_at" 20 | t.index ["user_id", "announcement_id"], name: "starburst_announcement_view_index", unique: true 21 | end 22 | 23 | create_table "starburst_announcements", force: :cascade do |t| 24 | t.text "title" 25 | t.text "body" 26 | t.datetime "start_delivering_at" 27 | t.datetime "stop_delivering_at" 28 | t.text "limit_to_users" 29 | t.datetime "created_at" 30 | t.datetime "updated_at" 31 | t.text "category" 32 | end 33 | 34 | create_table "users", force: :cascade do |t| 35 | t.string "subscription" 36 | t.datetime "created_at" 37 | t.datetime "updated_at" 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | arch: amd64 3 | os: linux 4 | dist: bionic 5 | env: 6 | global: 7 | - CC_TEST_REPORTER_ID=73e44c1f430ccd919b2549ec5e318608be412742ba67f260d1f815702a7f89b0 8 | language: ruby 9 | cache: bundler 10 | bundler_args: --with development 11 | gemfile: 12 | - gemfiles/rails_4.2.0.gemfile 13 | - gemfiles/rails_5.0.0.gemfile 14 | - gemfiles/rails_5.1.0.gemfile 15 | - gemfiles/rails_5.2.0.gemfile 16 | - gemfiles/rails_6.0.0.gemfile 17 | - gemfiles/rails_6.1.0.gemfile 18 | rvm: 19 | - 2.2 20 | - 2.3 21 | - 2.4 22 | - 2.5 23 | - 2.6 24 | - 2.7 25 | jobs: 26 | allow_failures: 27 | - gemfile: gemfiles/rails_5.2.0.gemfile 28 | rvm: 2.2 29 | exclude: 30 | - gemfile: gemfiles/rails_6.1.0.gemfile 31 | rvm: 2.4 32 | - gemfile: gemfiles/rails_6.1.0.gemfile 33 | rvm: 2.3 34 | - gemfile: gemfiles/rails_6.1.0.gemfile 35 | rvm: 2.2 36 | - gemfile: gemfiles/rails_6.0.0.gemfile 37 | rvm: 2.2 38 | - gemfile: gemfiles/rails_6.0.0.gemfile 39 | rvm: 2.3 40 | - gemfile: gemfiles/rails_6.0.0.gemfile 41 | rvm: 2.4 42 | - gemfile: gemfiles/rails_4.2.0.gemfile 43 | rvm: 2.6 44 | - gemfile: gemfiles/rails_4.2.0.gemfile 45 | rvm: 2.7 46 | before_install: 47 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true 48 | - gem install bundler -v '< 2' 49 | before_script: 50 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 51 | - chmod +x ./cc-test-reporter 52 | - ./cc-test-reporter before-build 53 | script: 54 | - bundle exec rspec 55 | after_script: 56 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 57 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | # Do not eager load code on boot. This avoids loading your whole application 13 | # just for the purpose of running a single test. If you are using a tool that 14 | # preloads Rails for running tests, you may have to set it to true. 15 | config.eager_load = false 16 | 17 | # Show full error reports and disable caching. 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates. 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment. 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Print deprecation notices to the stderr. 28 | config.active_support.deprecation = :stderr 29 | 30 | # Raises error for missing translations 31 | # config.action_view.raise_on_missing_translations = true 32 | 33 | # FactoryBot factories directories 34 | if RUBY_VERSION < '2.3.0' 35 | FactoryBot.definition_file_paths = [Rails.root.join('..', 'factories')] 36 | FactoryBot.reload 37 | else 38 | config.factory_bot.definition_file_paths = [Rails.root.join('..', 'factories')] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /app/models/starburst/announcement.rb: -------------------------------------------------------------------------------- 1 | module Starburst 2 | class Announcement < ActiveRecord::Base 3 | 4 | validates :body, presence: true 5 | 6 | serialize :limit_to_users 7 | 8 | scope :ready_for_delivery, lambda { 9 | where("(start_delivering_at < ? OR start_delivering_at IS NULL) 10 | AND (stop_delivering_at > ? OR stop_delivering_at IS NULL)", Time.current, Time.current) 11 | } 12 | 13 | scope :unread_by, lambda {|current_user| 14 | joins("LEFT JOIN starburst_announcement_views ON 15 | starburst_announcement_views.announcement_id = starburst_announcements.id AND 16 | starburst_announcement_views.user_id = #{sanitize_sql_for_conditions(current_user.id)}") 17 | .where("starburst_announcement_views.announcement_id IS NULL AND starburst_announcement_views.user_id IS NULL") 18 | } 19 | 20 | scope :in_delivery_order, lambda { order("start_delivering_at ASC")} 21 | 22 | def self.current(current_user) 23 | raise ArgumentError, 'User is required to find current announcement' unless current_user.present? 24 | 25 | find_announcement_for_current_user(ready_for_delivery.unread_by(current_user).in_delivery_order, current_user) 26 | end 27 | 28 | def self.find_announcement_for_current_user(announcements, user) 29 | user_as_array = user.serializable_hash(methods: Starburst.user_instance_methods) 30 | announcements.each do |announcement| 31 | if user_matches_conditions(user_as_array, announcement.limit_to_users) 32 | return announcement 33 | end 34 | end 35 | return nil 36 | end 37 | 38 | def self.user_matches_conditions(user, conditions = nil) 39 | if conditions 40 | conditions.each do |condition| 41 | if user[condition[:field]] != condition[:value] 42 | return false 43 | end 44 | end 45 | end 46 | return true 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/controllers/starburst/announcements_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Starburst::AnnouncementsController do 4 | routes { Starburst::Engine.routes } 5 | 6 | describe '#mark_as_read' do 7 | subject(:mark_as_read) do 8 | if Rails::VERSION::MAJOR < 5 9 | post :mark_as_read, **params 10 | else 11 | post :mark_as_read, params: params 12 | end 13 | end 14 | 15 | let(:announcement) { create(:announcement) } 16 | let(:params) { Hash[id: announcement.id] } 17 | 18 | before { allow(controller).to receive(:current_user).and_return(current_user) } 19 | 20 | context 'with a signed in user' do 21 | let(:current_user) { create(:user) } 22 | 23 | context 'when the user has not marked the announcement as read yet' do 24 | let(:announcement_view) { an_object_having_attributes(user_id: current_user.id) } 25 | 26 | it { expect(mark_as_read).to have_http_status(:ok) } 27 | 28 | it 'marks the announcement as viewed by the signed in user' do 29 | expect { mark_as_read }.to change(Starburst::AnnouncementView, :all).to contain_exactly(announcement_view) 30 | end 31 | end 32 | 33 | context 'when the user has already marked the announcement as read' do 34 | before { create(:announcement_view, user_id: current_user.id, announcement: announcement) } 35 | 36 | it { expect(mark_as_read).to have_http_status(:ok) } 37 | it { expect { mark_as_read }.not_to change(Starburst::AnnouncementView, :count) } 38 | end 39 | end 40 | 41 | context 'without a signed in user' do 42 | let(:current_user) { nil } 43 | 44 | it { expect(mark_as_read).to have_http_status(:unprocessable_entity) } 45 | it { expect { mark_as_read }.not_to change(Starburst::AnnouncementView, :count) } 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/features/announcements_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Announcements' do 4 | shared_examples 'announcement display' do 5 | let!(:announcement) { create(:announcement) } 6 | let(:user_id) { current_user.try(:id) } 7 | 8 | before { visit announcement_page } 9 | 10 | context 'when the user is signed in' do 11 | let(:current_user) { create(:user) } 12 | 13 | it 'displays the current announcement' do 14 | expect(page).to have_content(title) 15 | expect(page).to have_content(announcement.body) 16 | end 17 | 18 | it 'hides the announcement if the user closes it', js: true, driver: :selenium_chrome_headless do 19 | expect(page).to have_content(announcement.body) 20 | find('#starburst-close').click 21 | expect(page).not_to have_content(announcement.body) 22 | end 23 | end 24 | 25 | shared_examples 'when the user is not signed in' do 26 | let(:current_user) { nil } 27 | 28 | it 'does not display the current announcement' do 29 | expect(page).to have_content(title) 30 | expect(page).not_to have_content(announcement.body) 31 | end 32 | end 33 | end 34 | 35 | context 'with a Bootstrap layout' do 36 | let(:announcement_page) { bootstrap_path(user_id: user_id) } 37 | let(:title) { 'Bootstrap Announcement' } 38 | 39 | include_examples 'announcement display' 40 | end 41 | 42 | context 'with a Custom layout' do 43 | let(:announcement_page) { custom_path(user_id: user_id) } 44 | let(:title) { 'Custom Announcement' } 45 | 46 | include_examples 'announcement display' 47 | end 48 | 49 | context 'with a Foundation layout' do 50 | let(:announcement_page) { foundation_path(user_id: user_id) } 51 | let(:title) { 'Foundation Announcement' } 52 | 53 | include_examples 'announcement display' 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /starburst.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/starburst/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'starburst' 7 | spec.version = Starburst::VERSION 8 | spec.authors = ['Corey Martin'] 9 | spec.email = ['coreym@gmail.com'] 10 | 11 | spec.summary = 'One-time messages to users in your app' 12 | spec.description = 'Show one-time messages to users in your Rails app' 13 | spec.homepage = 'https://github.com/starburstgem/starburst' 14 | spec.license = 'MIT' 15 | 16 | spec.metadata = { 17 | 'bug_tracker_uri' => "#{spec.homepage}/issues", 18 | 'changelog_uri' => "#{spec.homepage}/blob/master/CHANGELOG.md", 19 | 'documentation_uri' => spec.homepage, 20 | 'homepage_uri' => spec.homepage, 21 | 'source_code_uri' => spec.homepage 22 | } 23 | 24 | spec.files = Dir['{app,config,db,lib}/**/*'] 25 | 26 | spec.extra_rdoc_files = Dir['CHANGELOG.md', 'LICENSE.txt', 'README.md'] 27 | spec.rdoc_options += [ 28 | '--title', 'Starburst', 29 | '--main', 'README.md', 30 | '--line-numbers', 31 | '--inline-source', 32 | '--quiet' 33 | ] 34 | 35 | spec.add_runtime_dependency 'rails', '>= 4.2.0', '< 6.2' 36 | 37 | spec.add_development_dependency 'appraisal', '~> 2' 38 | spec.add_development_dependency 'byebug', '>= 10.0.2', '< 12' 39 | spec.add_development_dependency 'capybara', '~> 3.1' 40 | spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8' 41 | spec.add_development_dependency 'factory_bot_rails', '>= 4.11.1', '< 6' 42 | spec.add_development_dependency 'rspec-rails', '~> 4.0' 43 | spec.add_development_dependency 'selenium-webdriver', '>= 3.141.0', '< 4' 44 | spec.add_development_dependency 'shoulda-matchers', '~> 4.3' 45 | spec.add_development_dependency 'simplecov', '>= 0.17.1', '< 1' 46 | spec.add_development_dependency 'sprockets-rails', '~> 3.2' 47 | spec.add_development_dependency 'sqlite3', '~> 1.4' 48 | spec.add_development_dependency 'webdrivers', '~> 4.0' 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/starburst_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Starburst do 4 | subject(:starburst) { described_class } 5 | 6 | around do |example| 7 | # copies original settings 8 | original_settings = described_class.class_variables.each_with_object({}) do |setting, configuration| 9 | configuration[setting] = described_class.class_variable_get(setting) 10 | end 11 | 12 | example.run 13 | 14 | # restores original settings back 15 | original_settings.each do |setting, value| 16 | described_class.class_variable_set(setting, value) 17 | end 18 | end 19 | 20 | describe '.base_controller' do 21 | subject(:base_controller) { starburst.base_controller } 22 | 23 | it { is_expected.to eq('::ApplicationController') } 24 | end 25 | 26 | describe '.base_controller=' do 27 | subject(:set_base_controller) { starburst.base_controller = new_base_controller } 28 | 29 | let(:new_base_controller) { 'CustomController' } 30 | 31 | it { expect { set_base_controller }.to change(starburst, :base_controller).to new_base_controller } 32 | end 33 | 34 | describe '.current_user_method' do 35 | subject(:current_user_method) { starburst.current_user_method } 36 | 37 | it { is_expected.to eq(:current_user) } 38 | end 39 | 40 | describe '.current_user_method=' do 41 | subject(:set_current_user_method) { starburst.current_user_method = new_current_user_method } 42 | 43 | let(:new_current_user_method) { :other_method } 44 | 45 | it { expect { set_current_user_method }.to change(starburst, :current_user_method).to new_current_user_method } 46 | end 47 | 48 | describe '.user_instance_methods' do 49 | subject(:user_instance_methods) { starburst.user_instance_methods } 50 | 51 | it { is_expected.to eq([]) } 52 | end 53 | 54 | describe '.user_instance_methods=' do 55 | subject(:set_user_instance_methods) { starburst.user_instance_methods = new_user_instance_methods } 56 | 57 | let(:new_user_instance_methods) { %i[active?] } 58 | 59 | it { expect { set_user_instance_methods }.to change(starburst, :user_instance_methods).to new_user_instance_methods } 60 | end 61 | 62 | describe '.configuration' do 63 | subject(:configuration) { starburst.configuration(&settings) } 64 | 65 | let(:new_base_controller) { 'CustomController' } 66 | let(:new_current_user_method) { :other_method } 67 | let(:new_user_instance_methods) { %i[active?] } 68 | let(:settings) do 69 | lambda do |config| 70 | config.base_controller = new_base_controller 71 | config.current_user_method = new_current_user_method 72 | config.user_instance_methods = new_user_instance_methods 73 | end 74 | end 75 | 76 | it 'updates Starburst settings' do 77 | expect { configuration }.to change(starburst, :base_controller).to(new_base_controller) 78 | .and(change(starburst, :current_user_method).to(new_current_user_method)) 79 | .and(change(starburst, :user_instance_methods).to(new_user_instance_methods)) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Enabling code coverage reporting 4 | require 'simplecov' 5 | 6 | # This file is copied to spec/ when you run 'rails generate rspec:install' 7 | require 'spec_helper' 8 | ENV['RAILS_ENV'] ||= 'test' 9 | require File.expand_path(File.join(__dir__, 'dummy', 'config', 'environment.rb')) 10 | require 'rspec/rails' 11 | # Add additional requires below this line. Rails is not loaded until this point! 12 | 13 | # Requires supporting ruby files with custom matchers and macros, etc, in 14 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 15 | # run as spec files by default. This means that files in spec/support that end 16 | # in _spec.rb will both be required and run as specs, causing the specs to be 17 | # run twice. It is recommended that you do not name files matching this glob to 18 | # end with _spec.rb. You can configure this pattern with the --pattern 19 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 20 | # 21 | # The following line is provided for convenience purposes. It has the downside 22 | # of increasing the boot-up time by auto-requiring all files in the support 23 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 24 | # require only the support files necessary. 25 | # 26 | 27 | Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each(&method(:require)) 28 | 29 | # Checks for pending migrations and applies them before tests are run. 30 | # If you are not using ActiveRecord, you can remove these lines. 31 | begin 32 | ActiveRecord::Migrator.migrations_paths = File.join(__dir__, 'dummy', 'db', 'migrate') 33 | ActiveRecord::Migration.maintain_test_schema! 34 | rescue ActiveRecord::PendingMigrationError => e 35 | puts e.to_s.strip 36 | exit 1 37 | end 38 | RSpec.configure do |config| 39 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 40 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 41 | 42 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 43 | # examples within a transaction, remove the following line or assign false 44 | # instead of true. 45 | # config.use_transactional_fixtures = true 46 | 47 | # You can uncomment this line to turn off ActiveRecord support entirely. 48 | # config.use_active_record = false 49 | 50 | # RSpec Rails can automatically mix in different behaviours to your tests 51 | # based on their file location, for example enabling you to call `get` and 52 | # `post` in specs under `spec/controllers`. 53 | # 54 | # You can disable this behaviour by removing the line below, and instead 55 | # explicitly tag your specs with their type, e.g.: 56 | # 57 | # RSpec.describe UsersController, type: :controller do 58 | # # ... 59 | # end 60 | # 61 | # The different available types are documented in the features, such as in 62 | # https://relishapp.com/rspec/rspec-rails/docs 63 | config.infer_spec_type_from_file_location! 64 | 65 | # Filter lines from Rails gems in backtraces. 66 | config.filter_rails_from_backtrace! 67 | # arbitrary gems may also be filtered via: 68 | # config.filter_gems_from_backtrace("gem name") 69 | end 70 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All complaints will be reviewed and 59 | investigated and will result in a response that is deemed necessary and 60 | appropriate to the circumstances. The project team is obligated to maintain 61 | confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 18 | RSpec.configure do |config| 19 | # rspec-expectations config goes here. You can use an alternate 20 | # assertion/expectation library such as wrong or the stdlib/minitest 21 | # assertions if you prefer. 22 | config.expect_with :rspec do |expectations| 23 | # This option will default to `true` in RSpec 4. It makes the `description` 24 | # and `failure_message` of custom matchers include text for helper methods 25 | # defined using `chain`, e.g.: 26 | # be_bigger_than(2).and_smaller_than(4).description 27 | # # => "be bigger than 2 and smaller than 4" 28 | # ...rather than: 29 | # # => "be bigger than 2" 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | # rspec-mocks config goes here. You can use an alternate test double 34 | # library (such as bogus or mocha) by changing the `mock_with` option here. 35 | config.mock_with :rspec do |mocks| 36 | # Prevents you from mocking or stubbing a method that does not exist on 37 | # a real object. This is generally recommended, and will default to 38 | # `true` in RSpec 4. 39 | mocks.verify_partial_doubles = true 40 | end 41 | 42 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 43 | # have no way to turn it off -- the option exists only for backwards 44 | # compatibility in RSpec 3). It causes shared context metadata to be 45 | # inherited by the metadata hash of host groups and examples, rather than 46 | # triggering implicit auto-inclusion in groups with matching metadata. 47 | config.shared_context_metadata_behavior = :apply_to_host_groups 48 | 49 | # This allows you to limit a spec run to individual examples or groups 50 | # you care about by tagging them with `:focus` metadata. When nothing 51 | # is tagged with `:focus`, all examples get run. RSpec also provides 52 | # aliases for `it`, `describe`, and `context` that include `:focus` 53 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 54 | config.filter_run_when_matching :focus 55 | 56 | # Allows RSpec to persist some state between runs in order to support 57 | # the `--only-failures` and `--next-failure` CLI options. We recommend 58 | # you configure your source control system to ignore this file. 59 | config.example_status_persistence_file_path = 'spec/examples.txt' 60 | 61 | # Limits the available syntax to the non-monkey patched syntax that is 62 | # recommended. For more details, see: 63 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 64 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 65 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 66 | # config.disable_monkey_patching! 67 | 68 | # Many RSpec users commonly either run the entire suite or an individual 69 | # file, and it's useful to allow more verbose output when running an 70 | # individual spec file. 71 | if config.files_to_run.one? 72 | # Use the documentation formatter for detailed output, 73 | # unless a formatter has already been configured 74 | # (e.g. via a command-line flag). 75 | config.default_formatter = 'doc' 76 | end 77 | 78 | # Print the 10 slowest examples and example groups at the 79 | # end of the spec run, to help surface which specs are running 80 | # particularly slow. 81 | config.profile_examples = 10 82 | 83 | # Run specs in random order to surface order dependencies. If you find an 84 | # order dependency and want to debug it, you can fix the order by providing 85 | # the seed, which is printed after each run. 86 | # --seed 1234 87 | config.order = :random 88 | 89 | # Seed global randomization in this process using the `--seed` CLI option. 90 | # Setting this allows you to use `--seed` to deterministically reproduce 91 | # test failures related to randomization by passing the same `--seed` value 92 | # as the one that triggered the failure. 93 | Kernel.srand config.seed 94 | end 95 | -------------------------------------------------------------------------------- /spec/models/starburst/announcement_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Starburst::Announcement do 4 | describe 'validations' do 5 | it { is_expected.to validate_presence_of(:body) } 6 | end 7 | 8 | describe '.current' do 9 | subject(:current) { described_class.current(user) } 10 | 11 | let(:user) { create(:user) } 12 | 13 | shared_examples 'oldest unread announcement' do 14 | it { is_expected.to eq(first_announcement) } 15 | 16 | context 'when a previous announcement has already been seen' do 17 | before { create(:announcement_view, user: user, announcement: first_announcement) } 18 | 19 | it { is_expected.to eq(second_announcement) } 20 | end 21 | end 22 | 23 | context 'when the provided user is nil' do 24 | let(:user) { nil } 25 | let(:message) { 'User is required to find current announcement' } 26 | 27 | it { expect { current }.to raise_error(ArgumentError).with_message(message) } 28 | end 29 | 30 | context 'when it is expired' do 31 | before { create(:announcement, stop_delivering_at: 1.minute.ago) } 32 | 33 | it { is_expected.to be_nil } 34 | end 35 | 36 | context 'when it is not expired yet' do 37 | let!(:first_announcement) do 38 | create( 39 | :announcement, 40 | start_delivering_at: 2.minutes.ago, 41 | stop_delivering_at: 10.minutes.from_now 42 | ) 43 | end 44 | let!(:second_announcement) do 45 | create( 46 | :announcement, 47 | start_delivering_at: 1.minute.ago, 48 | stop_delivering_at: 10.minutes.from_now 49 | ) 50 | end 51 | 52 | include_examples 'oldest unread announcement' 53 | end 54 | 55 | context 'when it is due' do 56 | let!(:first_announcement) { create(:announcement, start_delivering_at: 2.minutes.ago) } 57 | let!(:second_announcement) { create(:announcement, start_delivering_at: 1.minute.ago) } 58 | 59 | include_examples 'oldest unread announcement' 60 | end 61 | 62 | context 'when it is not due yet' do 63 | before { create(:announcement, start_delivering_at: 1.minute.from_now) } 64 | 65 | it { is_expected.to be_nil } 66 | end 67 | 68 | context 'when no timestamps are set' do 69 | let!(:first_announcement) { create(:announcement, start_delivering_at: nil, stop_delivering_at: nil) } 70 | let!(:second_announcement) { create(:announcement, start_delivering_at: nil, stop_delivering_at: nil) } 71 | 72 | include_examples 'oldest unread announcement' 73 | end 74 | 75 | context 'when there are announcements with an attribute condition' do 76 | let!(:first_announcement) do 77 | create( 78 | :announcement, 79 | start_delivering_at: 2.minutes.ago, 80 | limit_to_users: [ 81 | { 82 | field: 'subscription', 83 | value: 'weekly' 84 | } 85 | ] 86 | ) 87 | end 88 | let!(:second_announcement) do 89 | create( 90 | :announcement, 91 | start_delivering_at: 1.minutes.ago, 92 | limit_to_users: [ 93 | { 94 | field: 'subscription', 95 | value: 'weekly' 96 | } 97 | ] 98 | ) 99 | end 100 | 101 | context 'when the user should see the announcements' do 102 | let(:user) { create(:user, subscription: 'weekly') } 103 | 104 | include_examples 'oldest unread announcement' 105 | end 106 | 107 | context 'when the user should not see the announcements' do 108 | let(:user) { create(:user, subscription: 'monthly') } 109 | 110 | it { is_expected.to be_nil } 111 | end 112 | end 113 | 114 | context 'when there are announcements with a method condition' do 115 | let!(:first_announcement) do 116 | create( 117 | :announcement, 118 | start_delivering_at: 2.minutes.ago, 119 | limit_to_users: [ 120 | { 121 | field: 'free?', 122 | value: true 123 | } 124 | ] 125 | ) 126 | end 127 | let!(:second_announcement) do 128 | create( 129 | :announcement, 130 | start_delivering_at: 1.minute.ago, 131 | limit_to_users: [ 132 | { 133 | field: 'free?', 134 | value: true 135 | } 136 | ] 137 | ) 138 | end 139 | 140 | before { allow(Starburst).to receive(:user_instance_methods).and_return(%i[free?]) } 141 | 142 | context 'when the user should see the announcement' do 143 | let(:user) { create(:user, subscription: '') } 144 | 145 | include_examples 'oldest unread announcement' 146 | end 147 | 148 | context 'when the user should not see the announcement' do 149 | let(:user) { create(:user, subscription: 'monthly') } 150 | 151 | it { is_expected.to be_nil } 152 | end 153 | end 154 | end 155 | 156 | describe '.in_delivery_order' do 157 | subject { described_class.in_delivery_order } 158 | 159 | let!(:first_announcement) { create(:announcement, start_delivering_at: 2.minutes.ago) } 160 | let!(:second_announcement) { create(:announcement, start_delivering_at: 1.minute.ago) } 161 | 162 | it { is_expected.to eq([first_announcement, second_announcement]) } 163 | end 164 | 165 | describe '.ready_for_delivery' do 166 | subject { described_class.ready_for_delivery } 167 | 168 | let!(:due_announcement) { create(:announcement, start_delivering_at: 1.minute.ago) } 169 | let!(:not_due_announcement) { create(:announcement, start_delivering_at: 1.minute.from_now) } 170 | let!(:expired_announcement) { create(:announcement, stop_delivering_at: 1.minute.ago) } 171 | let!(:not_expired_announcement) { create(:announcement, stop_delivering_at: 1.minute.from_now) } 172 | let!(:unscheduled_announcement) { create(:announcement, start_delivering_at: nil, stop_delivering_at: nil) } 173 | 174 | it { is_expected.to contain_exactly(due_announcement, not_expired_announcement, unscheduled_announcement) } 175 | end 176 | 177 | describe '.unread_by' do 178 | subject { described_class.unread_by(current_user) } 179 | 180 | let(:current_user) { create(:user) } 181 | let(:another_user) { create(:user) } 182 | let(:announcement1) { create(:announcement) } 183 | let(:announcement2) { create(:announcement) } 184 | 185 | before do 186 | create(:announcement_view, user: another_user, announcement: announcement1) 187 | create(:announcement_view, user: current_user, announcement: announcement2) 188 | end 189 | 190 | it { is_expected.to contain_exactly(announcement1) } 191 | end 192 | 193 | describe '.find_announcement_for_current_user' do 194 | subject { described_class.find_announcement_for_current_user(described_class.all, user) } 195 | 196 | context 'with an attribute condition' do 197 | let!(:announcement) do 198 | create( 199 | :announcement, 200 | limit_to_users: [ 201 | { 202 | field: 'subscription', 203 | value: 'weekly' 204 | } 205 | ] 206 | ) 207 | end 208 | 209 | context 'when the user should see the announcement' do 210 | let(:user) { create(:user, subscription: 'weekly') } 211 | 212 | it { is_expected.to eq(announcement) } 213 | end 214 | 215 | context 'when the user should not see the announcement' do 216 | let(:user) { create(:user, subscription: 'monthly') } 217 | 218 | it { is_expected.to be_nil } 219 | end 220 | end 221 | 222 | context 'with a method condition' do 223 | let!(:announcement) do 224 | create( 225 | :announcement, 226 | limit_to_users: [ 227 | { 228 | field: 'free?', 229 | value: true 230 | } 231 | ] 232 | ) 233 | end 234 | 235 | before { allow(Starburst).to receive(:user_instance_methods).and_return(%i[free?]) } 236 | 237 | context 'when the user should see the announcement' do 238 | let(:user) { create(:user, subscription: '') } 239 | 240 | it { is_expected.to eq(announcement) } 241 | end 242 | 243 | context 'when the user should not see the announcement' do 244 | let(:user) { create(:user, subscription: 'monthly') } 245 | 246 | it { is_expected.to be_nil } 247 | end 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starburst 2 | 3 | [![Build Status](https://travis-ci.org/starburstgem/starburst.svg?branch=master)](https://travis-ci.org/starburstgem/starburst) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/7f5df24aaefecbd270b4/maintainability)](https://codeclimate.com/github/starburstgem/starburst/maintainability) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/7f5df24aaefecbd270b4/test_coverage)](https://codeclimate.com/github/starburstgem/starburst/test_coverage) 6 | 7 | Starburst allows you to show messages to logged in users within your Rails app. Once the user closes the message, they won't see it again. 8 | 9 | You can target messages to particular groups of users, based on their database attributes or your own methods on the `User` class. For instance, you might send a message only to users on your premium plan. 10 | 11 | Starburst remembers _on the server_ who has closed which message. Therefore, a user who closes a message on their desktop won't see it again on their mobile device. Starburst doesn't use cookies, so a user won't see an announcement they've already read if they switch devices or clear their cookies. 12 | 13 | Starburst is in production on [Cook Smarts](https://www.cooksmarts.com), where it has served announcements to thousands of users. 14 | 15 | [![Announcement in ZURB Foundation](http://aspiringwebdev.com/wp-content/uploads/2014/10/starburst-foundation.png)](#) 16 | 17 | _An announcement delivered by Starburst, on a Rails app using ZURB Foundation_ 18 | 19 | ## Use cases 20 | 21 | Use Starburst to share announcements with your users, like: 22 | 23 | - A new feature 24 | - Upcoming downtime 25 | - A coupon to upgrade to a premium plan 26 | 27 | Users will see the message until they dismiss it, and then won't see it again. 28 | 29 | A user will not see the next announcement until they acknowledge the previous one (i.e. users are shown one announcement at a time). Announcements are delivered earliest first. Be sure to [schedule](#scheduling-announcements) announcements so they appear only while they're relevant. 30 | 31 | ## Requirements 32 | 33 | ### Authentication 34 | Starburst needs to know who is logged in to your app. If you are using [Devise](https://github.com/heartcombo/devise), [Clearance](https://github.com/thoughtbot/clearance), or another authentication library that sets a `current_user` method, you're all set. 35 | 36 | If you use a different authentication system that does not set a `current_user` method, [tell Starburst](#current-user) what method your library uses. 37 | 38 | ### Ruby and Rails 39 | 40 | Starburst [works](https://secure.travis-ci.org/starburstgem/starburst) on Rails 4.2, 5.0, 5.1, 5.2, 6.0, and 6.1. It should work with the same Ruby runtime versions supported by each framework version. 41 | 42 | ## Installation 43 | 44 | Add Starburst to your `Gemfile`: 45 | 46 | ```ruby 47 | gem 'starburst' 48 | ``` 49 | 50 | Migrate your database: 51 | 52 | ```sh 53 | $ rake starburst:install:migrations 54 | $ rake db:migrate 55 | ``` 56 | 57 | Add the following line to your `ApplicationController`: 58 | 59 | ```ruby 60 | helper Starburst::AnnouncementsHelper 61 | ``` 62 | 63 | Add the following line to your routes file: 64 | 65 | ```ruby 66 | mount Starburst::Engine => '/starburst' 67 | ``` 68 | 69 | Add the following line to your `application.js` file: 70 | 71 | ```javascript 72 | //= require starburst/starburst 73 | ``` 74 | 75 | ## Getting started 76 | 77 | ### Add an announcement partial to your app's layout 78 | 79 | Starburst comes with pre-built announcement boxes for sites using [ZURB Foundation](https://get.foundation) and [Bootstrap](https://getbootstrap.com). It also includes an announcement box with no assigned styles. 80 | 81 | Add one of the lines below your application layout view at `app/views/layouts/application.html.erb`, right above `<%= yield =>`. You can place the partials anywhere, of course; this is just the most common location. 82 | 83 | #### Bootstrap 84 | 85 | ```erb 86 | <%= render partial: 'announcements/starburst/announcement_bootstrap' %> 87 | ``` 88 | 89 | #### ZURB Foundation 90 | 91 | ```erb 92 | <%= render partial: 'announcements/starburst/announcement_foundation' %> 93 | ``` 94 | 95 | #### Custom styles 96 | 97 | ```erb 98 | <%= render partial: 'announcements/starburst/announcement' %> 99 | ``` 100 | 101 | Set your own styles. Use `#starburst-announcement` ID for the box, and the `#starburst-close` for the close button. 102 | 103 | ### Add an announcement 104 | 105 | Starburst doesn't have an admin interface yet, but you can add announcements through your own code. 106 | 107 | ```ruby 108 | Starburst::Announcement.create(body: 'Our app now features lots of balloons! Enjoy!') 109 | ``` 110 | 111 | This will present an announcement to every user of the app. Once they dismiss the announcement, they won't see it again. 112 | 113 | Find out more about [scheduling announcements](#scheduling-announcements) and [targeting them to specific users](#targeting-announcements). 114 | 115 | ## Scheduling announcements 116 | 117 | You can schedule announcements as follows: 118 | 119 | `start_delivering_at` - Do not deliver this announcement until this date. 120 | 121 | `stop_delivering_at` - Do not show this announcement to anyone after this date, not even to users who have seen the message before but not acknowledged it. 122 | 123 | ```ruby 124 | Starburst::Announcement.create( 125 | body: 'Our app now features lots of balloons! Enjoy!', 126 | start_delivering_at: Date.current, 127 | stop_delivering_at: Date.current.advance(days: 10) 128 | ) 129 | ``` 130 | 131 | ## Targeting announcements 132 | 133 | You can target announcements to particular users by setting the `limit_to_users` option. 134 | 135 | The code below targets the announcement to users with a `subscription` field equal to `gold`. 136 | 137 | ```ruby 138 | Starburst::Announcement.create( 139 | body: 'Upgrade to platinum and save 10% with coupon code XYZ!', 140 | limit_to_users: [ 141 | { 142 | field: 'subscription', 143 | value: 'gold' 144 | } 145 | ] 146 | ) 147 | ``` 148 | 149 | ## Advanced configuration 150 | 151 | ### Base controller 152 | 153 | By default, `Starburst::AnnouncementsController` will inherit from `ApplicationController`. If you need to change that setting in order to have access to the configured `current_user_method`, just change the `base_controller` setting: 154 | 155 | ```ruby 156 | Starburst.configuration do |config| 157 | config.base_controller = 'AuthenticatedController' 158 | end 159 | ``` 160 | 161 | ### Current user 162 | 163 | Most Rails authentication libraries (like Devise and Clearance) place the current user into the `current_user` method. If your authentication library uses a different method, create an initializer file for Starburst at `config/initializers/starburst.rb` and add the code below, replacing `current_user` with the name of the equivalent method in your authentication library. 164 | 165 | ```ruby 166 | Starburst.configuration do |config| 167 | config.current_user_method = :current_user 168 | end 169 | ``` 170 | 171 | ### Targeting by methods rather than fields 172 | 173 | With targeting, you can limit which users will see a particular announcement. Out of the box, Starburst allows you to limit by fields in the database. However, your `User` model may have methods that don't directly map to database fields. 174 | 175 | For instance, your `User` model might have an instance method `free?` that returns `true` if the user is on a free plan and `false` if they are on a paid plan. The actual field in the database may be called something different. 176 | 177 | You can target based on methods that are not in the database, but you must specify those methods in a Starburst initializer. Create an initializer for Starburst at `config/initializers/starburst.rb` and add the code below: 178 | 179 | ```ruby 180 | Starburst.configuration do |config| 181 | config.user_instance_methods = %i[free?] 182 | end 183 | ``` 184 | 185 | `user_instance_methods` is an array, so you can specify more than one method. All of the methods will be available for [targeting](#targeting-announcements), as if they were fields. 186 | 187 | ## Upgrading 188 | 189 | ### From 0.9.x to 1.0 190 | 191 | This guidance applies only to users of Starburst who are upgrading from 0.9.x to 1.0. 192 | 193 | **IMPORTANT**: This version introduces a uniqueness constraint on the `AnnouncementView` table. Before installing, you must find and clear out duplicate announcement views in the table: 194 | 195 | 1. In console on both the development and production environments, run this to find duplicate announcement views: 196 | ```ruby 197 | Starburst::AnnouncementView.select(%i[announcement_id user_id]).group(:announcement_id, :user_id).having('COUNT(*) > 1').count 198 | ``` 199 | 2. Delete duplicate announcement views as necessary so you have only one of each. Use the `destroy` method on each individual view 200 | 3. Run `rake starburst:install:migrations` and then `rake db:migrate` on your development environment, then push to production 201 | 202 | ## Contributors 203 | 204 | Thanks to [@jhenkens](https://github.com/jhenkens) for fixes, performance improvements, and ideas for showing announcements to non-logged-in users. 205 | --------------------------------------------------------------------------------