Merge pull request #70 from Retrospring/feature/announcements

Implement Announcements
This commit is contained in:
Dominik M. Kwiatek 2020-04-19 23:10:47 +01:00 committed by GitHub
commit 516bc48aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 429 additions and 0 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
module AnnouncementHelper
end

View File

@ -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

View File

@ -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"

View File

@ -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?'

View File

@ -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"

View File

@ -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

View File

@ -25,6 +25,7 @@
= csrf_meta_tags
%body#version1
= render 'layouts/header'
= render 'shared/announcements'
= yield
= render 'shared/locales'
- if Rails.env.development?

View File

@ -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" } &times;
%p= announcement.content
- if announcement.link_present?
%a.alert-link{ href: announcement.link_href }= announcement.link_text

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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