Merge pull request #70 from Retrospring/feature/announcements
Implement Announcements
This commit is contained in:
commit
516bc48aa0
2
Gemfile
2
Gemfile
|
@ -105,4 +105,6 @@ group :development, :test do
|
|||
gem 'letter_opener' # Use this just in local test environments
|
||||
gem 'brakeman'
|
||||
gem 'guard-brakeman'
|
||||
gem 'timecop'
|
||||
gem 'rails-controller-testing'
|
||||
end
|
||||
|
|
|
@ -344,6 +344,10 @@ GEM
|
|||
rails-assets-growl (1.3.5)
|
||||
rails-assets-jquery
|
||||
rails-assets-jquery (2.2.4)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
actionview (>= 5.0.1.x)
|
||||
activesupport (>= 5.0.1.x)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
|
@ -459,6 +463,7 @@ GEM
|
|||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
timecop (0.9.1)
|
||||
tiny-color-rails (0.0.2)
|
||||
railties (>= 3.0)
|
||||
turbolinks (2.5.4)
|
||||
|
@ -553,6 +558,7 @@ DEPENDENCIES
|
|||
rails (~> 5.2)
|
||||
rails-assets-growl!
|
||||
rails-assets-jquery (~> 2.2.0)!
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 5.0)
|
||||
rails_admin
|
||||
rake
|
||||
|
@ -570,6 +576,7 @@ DEPENDENCIES
|
|||
simplecov-rcov
|
||||
spring (~> 2.0)
|
||||
sweetalert-rails
|
||||
timecop
|
||||
tiny-color-rails
|
||||
tumblr_client!
|
||||
turbolinks (~> 2.5.3)
|
||||
|
|
|
@ -74,6 +74,16 @@ _ready = ->
|
|||
lineColor: bodyColor
|
||||
density: 23000
|
||||
|
||||
$(".alert-announcement").each ->
|
||||
aId = $(this)[0].dataset.announcementId
|
||||
unless (window.localStorage.getItem("announcement#{aId}"))
|
||||
$(this).toggleClass("hidden")
|
||||
|
||||
$(document).on "click", ".alert-announcement button.close", (evt) ->
|
||||
announcement = event.target.closest(".alert-announcement")
|
||||
aId = announcement.dataset.announcementId
|
||||
window.localStorage.setItem("announcement#{aId}", true)
|
||||
|
||||
$('.arctic_scroll').arctic_scroll speed: 500
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
class AnnouncementController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@announcements = Announcement.all
|
||||
end
|
||||
|
||||
def new
|
||||
@announcement = Announcement.new
|
||||
end
|
||||
|
||||
def create
|
||||
@announcement = Announcement.new(announcement_params)
|
||||
@announcement.user = current_user
|
||||
if @announcement.save
|
||||
flash[:success] = "Announcement created successfully."
|
||||
redirect_to action: :index
|
||||
else
|
||||
render 'announcement/new'
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@announcement = Announcement.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@announcement = Announcement.find(params[:id])
|
||||
@announcement.update(announcement_params)
|
||||
if @announcement.save
|
||||
flash[:success] = "Announcement updated successfully."
|
||||
redirect_to announcement_index_path
|
||||
else
|
||||
render 'announcement/edit'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if Announcement.destroy(params[:id])
|
||||
flash[:success] = "Announcement deleted successfully."
|
||||
else
|
||||
flash[:error] = "Failed to delete announcement."
|
||||
end
|
||||
redirect_to announcement_index_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def announcement_params
|
||||
params.require(:announcement).permit(:content, :link_text, :link_href, :starts_at, :ends_at)
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
|
|||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
before_action :check_locale
|
||||
before_action :banned?
|
||||
before_action :find_active_announcements
|
||||
|
||||
# check if user wants to read
|
||||
def check_locale
|
||||
|
@ -50,6 +51,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def find_active_announcements
|
||||
@active_announcements ||= Announcement.find_active
|
||||
end
|
||||
|
||||
include ApplicationHelper
|
||||
|
||||
protected
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
module AnnouncementHelper
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
class Announcement < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :content, presence: true
|
||||
validates :starts_at, presence: true
|
||||
validates :link_href, presence: true, if: -> { link_text.present? }
|
||||
validate :starts_at, :validate_date_range
|
||||
|
||||
def self.find_active
|
||||
Rails.cache.fetch "announcement_active", expires_in: 1.minute do
|
||||
where "starts_at <= :now AND ends_at > :now", now: Time.current
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
Time.now.utc >= starts_at && Time.now.utc < ends_at
|
||||
end
|
||||
|
||||
def link_present?
|
||||
link_text.present?
|
||||
end
|
||||
|
||||
def validate_date_range
|
||||
if starts_at > ends_at
|
||||
errors.add(:starts_at, "Start date must be before end date")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
- provide(:title, generate_title("Edit announcement"))
|
||||
.container.j2-page
|
||||
= bootstrap_form_for(@announcement, url: {action: "update"}, method: "PATCH") do |f|
|
||||
- if @announcement.errors.any?
|
||||
.row
|
||||
.col-md-12
|
||||
.alert.alert-danger
|
||||
%strong
|
||||
= pluralize(@announcement.errors.count, "error")
|
||||
prohibited this announcement from being saved:
|
||||
%ul
|
||||
- @announcement.errors.full_messages.each do |err|
|
||||
%li= err
|
||||
.row
|
||||
.col-md-12
|
||||
= f.text_area :content, label: "Content"
|
||||
.row
|
||||
.col-md-6
|
||||
= f.url_field :link_href, label: "Link URL"
|
||||
.col-md-6
|
||||
= f.datetime_field :link_text, label: "Link text"
|
||||
.row
|
||||
.col-md-6
|
||||
= f.datetime_field :starts_at, label: "Start time"
|
||||
.col-md-6
|
||||
= f.datetime_field :ends_at, label: "End time"
|
||||
.row
|
||||
.col-md-12.text-right
|
||||
= f.submit class: "btn btn-primary"
|
|
@ -0,0 +1,14 @@
|
|||
- provide(:title, generate_title("Announcements"))
|
||||
.container.j2-page
|
||||
.row
|
||||
.col-md-12
|
||||
= link_to "Add new", :announcement_new, class: "btn btn-default"
|
||||
- @announcements.each do |announcement|
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
= announcement.starts_at
|
||||
.panel-body
|
||||
= announcement.content
|
||||
.panel-footer
|
||||
= button_to "Edit", announcement_edit_path(id: announcement.id), method: :get, class: 'btn btn-link'
|
||||
= button_to "Delete", announcement_destroy_path(id: announcement.id), method: :delete, class: 'btn btn-link', confirm: 'Are you sure you want to delete this announcement?'
|
|
@ -0,0 +1,29 @@
|
|||
- provide(:title, generate_title("Add new announcement"))
|
||||
.container.j2-page
|
||||
= bootstrap_form_for(@announcement, url: {action: "create"}) do |f|
|
||||
- if @announcement.errors.any?
|
||||
.row
|
||||
.col-md-12
|
||||
.alert.alert-danger
|
||||
%strong
|
||||
= pluralize(@announcement.errors.count, "error")
|
||||
prohibited this announcement from being saved:
|
||||
%ul
|
||||
- @announcement.errors.full_messages.each do |err|
|
||||
%li= err
|
||||
.row
|
||||
.col-md-12
|
||||
= f.text_area :content, label: "Content"
|
||||
.row
|
||||
.col-md-6
|
||||
= f.url_field :link_href, label: "Link URL"
|
||||
.col-md-6
|
||||
= f.datetime_field :link_text, label: "Link text"
|
||||
.row
|
||||
.col-md-6
|
||||
= f.datetime_field :starts_at, label: "Start time"
|
||||
.col-md-6
|
||||
= f.datetime_field :ends_at, label: "End time"
|
||||
.row
|
||||
.col-md-12.text-right
|
||||
= f.submit class: "btn btn-primary"
|
|
@ -28,6 +28,10 @@
|
|||
%a{href: pghero_path}
|
||||
%i.fa.fa-fw.fa-database
|
||||
Database Monitor
|
||||
%li
|
||||
%a{href: announcement_index_path}
|
||||
%i.fa.fa-fw.fa-info
|
||||
Announcements
|
||||
%li.divider
|
||||
- if current_user.mod?
|
||||
%li
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
= csrf_meta_tags
|
||||
%body#version1
|
||||
= render 'layouts/header'
|
||||
= render 'shared/announcements'
|
||||
= yield
|
||||
= render 'shared/locales'
|
||||
- if Rails.env.development?
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.container.announcements
|
||||
- @active_announcements.each do |announcement|
|
||||
.alert.alert-announcement.alert-info.alert-dismissable.hidden{ data: { 'announcement-id': announcement.id } }
|
||||
%button.close{ type: "button", "data-dismiss" => "alert" }
|
||||
%span{ "aria-hidden" => "true" } ×
|
||||
%p= announcement.content
|
||||
- if announcement.link_present?
|
||||
%a.alert-link{ href: announcement.link_href }= announcement.link_text
|
|
@ -10,6 +10,13 @@ Rails.application.routes.draw do
|
|||
|
||||
mount Sidekiq::Web, at: "/sidekiq"
|
||||
mount PgHero::Engine, at: "/pghero", as: "pghero"
|
||||
|
||||
match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index
|
||||
match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create
|
||||
match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new
|
||||
match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit
|
||||
match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update
|
||||
match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy
|
||||
end
|
||||
|
||||
# Moderation panel
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
class CreateAnnouncements < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :announcements do |t|
|
||||
t.text :content, null: false
|
||||
t.string :link_text
|
||||
t.string :link_href
|
||||
t.datetime :starts_at, null: false
|
||||
t.datetime :ends_at, null: false
|
||||
t.belongs_to :user, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
12
db/schema.rb
12
db/schema.rb
|
@ -15,6 +15,18 @@ ActiveRecord::Schema.define(version: 2020_04_19_185535) do
|
|||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "announcements", force: :cascade do |t|
|
||||
t.text "content", null: false
|
||||
t.string "link_text"
|
||||
t.string "link_href"
|
||||
t.datetime "starts_at", null: false
|
||||
t.datetime "ends_at", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id"], name: "index_announcements_on_user_id"
|
||||
end
|
||||
|
||||
create_table "answers", id: :serial, force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.integer "question_id"
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe AnnouncementController, type: :controller do
|
||||
let(:user) { FactoryBot.create(:user, roles: [:administrator]) }
|
||||
|
||||
describe "#index" do
|
||||
subject { get :index }
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "renders the index template" do
|
||||
subject
|
||||
expect(response).to render_template(:index)
|
||||
end
|
||||
|
||||
context "no announcements" do
|
||||
it "@announcements is empty" do
|
||||
subject
|
||||
expect(assigns(:announcements)).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "one announcement" do
|
||||
let!(:announcement) { Announcement.create(content: "I am announcement", user: user, starts_at: Time.current, ends_at: Time.current + 2.days) }
|
||||
|
||||
it "includes the announcement in the @announcements assign" do
|
||||
subject
|
||||
expect(assigns(:announcements)).to include(announcement)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#new" do
|
||||
subject { get :new }
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "renders the new template" do
|
||||
subject
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
let :announcement_params do
|
||||
{
|
||||
announcement: {
|
||||
content: "I like dogs!",
|
||||
starts_at: Time.current,
|
||||
ends_at: Time.current + 2.days
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
subject { post :create, params: announcement_params }
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "creates an announcement" do
|
||||
expect { subject }.to change { Announcement.count }.by(1)
|
||||
end
|
||||
|
||||
it "redirects to announcement#index" do
|
||||
subject
|
||||
expect(response).to redirect_to(:announcement_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#edit" do
|
||||
let! :announcement do
|
||||
Announcement.create(content: "Dogs are pretty cool, I guess",
|
||||
starts_at: Time.current + 3.days,
|
||||
ends_at: Time.current + 10.days,
|
||||
user: user)
|
||||
end
|
||||
|
||||
subject { get :edit, params: { id: announcement.id } }
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "renders the edit template" do
|
||||
subject
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update" do
|
||||
let :announcement_params do
|
||||
{
|
||||
content: "The trebuchet is the superior siege weapon"
|
||||
}
|
||||
end
|
||||
|
||||
let! :announcement do
|
||||
Announcement.create(content: "Dogs are pretty cool, I guess",
|
||||
starts_at: Time.current + 3.days,
|
||||
ends_at: Time.current + 10.days,
|
||||
user: user)
|
||||
end
|
||||
|
||||
subject do
|
||||
patch :update, params: {
|
||||
id: announcement.id,
|
||||
announcement: announcement_params
|
||||
}
|
||||
end
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "updates the announcement" do
|
||||
subject
|
||||
updated = Announcement.find announcement.id
|
||||
expect(updated.content).to eq(announcement_params[:content])
|
||||
end
|
||||
|
||||
it "redirects to announcement#index" do
|
||||
subject
|
||||
expect(response).to redirect_to(:announcement_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#destroy" do
|
||||
let! :announcement do
|
||||
Announcement.create(content: "Dogs are pretty cool, I guess",
|
||||
starts_at: Time.current + 3.days,
|
||||
ends_at: Time.current + 10.days,
|
||||
user: user)
|
||||
end
|
||||
|
||||
subject { delete :destroy, params: { id: announcement.id } }
|
||||
|
||||
context "user signed in" do
|
||||
before(:each) { sign_in(user) }
|
||||
|
||||
it "deletes the announcement" do
|
||||
expect { subject }.to change { Announcement.count }.by(-1)
|
||||
end
|
||||
|
||||
it "redirects to announcement#index" do
|
||||
subject
|
||||
expect(response).to redirect_to(:announcement_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe(Announcement, type: :model) do
|
||||
let!(:user) { FactoryBot.create :user }
|
||||
let!(:me) do
|
||||
Announcement.new(
|
||||
content: "Raccoon",
|
||||
starts_at: Time.current,
|
||||
ends_at: Time.current + 1.day,
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
describe "#active?" do
|
||||
it "returns true when the current time is between starts_at and ends_at" do
|
||||
expect(me.active?).to be(true)
|
||||
end
|
||||
|
||||
it "returns false when the current time is before starts_at" do
|
||||
Timecop.freeze(me.starts_at - 1.second)
|
||||
expect(me.active?).to be(false)
|
||||
Timecop.return
|
||||
end
|
||||
|
||||
it "returns false when the current time is after ends_at" do
|
||||
Timecop.freeze(me.ends_at)
|
||||
expect(me.active?).to be(false)
|
||||
Timecop.return
|
||||
end
|
||||
end
|
||||
|
||||
describe "#link_present?" do
|
||||
it "returns true if a link is present" do
|
||||
me.link_text = "Very good dogs"
|
||||
me.link_href = "https://www.reddit.com/r/rarepuppers/"
|
||||
expect(me.link_present?).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if a link is not present" do
|
||||
me.link_text = nil
|
||||
me.link_href = nil
|
||||
expect(me.link_present?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue