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 | [](https://travis-ci.org/starburstgem/starburst)
4 | [](https://codeclimate.com/github/starburstgem/starburst/maintainability)
5 | [](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 | [](#)
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 |
--------------------------------------------------------------------------------