21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-28 15:00:50 +02:00

Add & removal of notification from a check

This commit is contained in:
Colin Darie 2018-06-04 20:39:53 +02:00
parent 1915a93848
commit b952f600f1
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
25 changed files with 354 additions and 15 deletions

View file

@ -39,6 +39,9 @@ gem 'bcrypt', '~> 3.1.7'
gem 'open4'
gem 'naught'
gem 'octicons'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

View file

@ -175,6 +175,8 @@ GEM
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
octicons (7.3.0)
nokogiri (>= 1.6.3.1)
open4 (1.3.4)
orm_adapter (0.5.0)
parallel (1.12.1)
@ -335,6 +337,7 @@ DEPENDENCIES
listen (>= 3.0.5, < 3.2)
mysql2 (>= 0.4.4, < 0.6.0)
naught
octicons
open4
pry-byebug
pry-rails

View file

@ -10,6 +10,7 @@ class ChecksController < ApplicationController
def new
@check = Check.new
build_empty_notification
authorize @check
end
@ -27,7 +28,9 @@ class ChecksController < ApplicationController
end
end
def edit; end
def edit
build_empty_notification
end
def update
if @check.update(update_check_params)
@ -35,6 +38,7 @@ class ChecksController < ApplicationController
redirect_to checks_path
else
flash.now[:alert] = "An error occured."
build_empty_notification
render :edit
end
end
@ -62,6 +66,11 @@ class ChecksController < ApplicationController
end
def check_params(*others)
params.require(:check).permit(:domain, :domain_created_at, :comment, :vendor, *others)
params.require(:check).permit(:domain, :domain_created_at, :comment, :vendor, *others,
notifications_attributes: [:id, :channel, :recipient, :delay])
end
def build_empty_notification
@check.notifications.build
end
end

View file

@ -0,0 +1,44 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!
before_action :set_notification, except: [:create]
def create
check = Check.find(params[:check_id])
@notification = check.notifications.build(notification_params)
authorize @notification
if @notification.save
flash[:notice] = "Your notification has been saved."
redirect_to check_path
else
flash.now[:alert] = "An error occured."
render "checks/edit"
end
end
def destroy
@notification.destroy!
respond_to do |format|
format.js
end
end
private
def set_notification
# joins the check because policy use the check relation
@notification = Notification
.joins(:check)
.find_by!(id: params[:id], check_id: params[:check_id])
authorize @notification
end
def notification_params
params.require(:notification).permit(:channel, :recipient, :delay)
end
def check_path
edit_check_path(check_id: params[:check_id])
end
end

View file

@ -0,0 +1,5 @@
.octicon {
fill: currentColor;
vertical-align: text-top;
display: inline-block;
}

View file

@ -1,3 +1,4 @@
@import '~bootstrap/scss/bootstrap';
@import 'layout';
@import 'icons';
@import 'components/users';

View file

@ -1,2 +1,9 @@
module NotificationsHelper
def many_channels_available?
Notification.channels.many?
end
def recipient_col_class
many_channels_available? ? "col-md-7" : "col-md-9"
end
end

View file

@ -29,7 +29,10 @@
class Check < ApplicationRecord
belongs_to :user
has_many :logs, class_name: "CheckLog"
has_many :notifications
has_many :notifications, validate: true, dependent: :destroy
accepts_nested_attributes_for :notifications,
allow_destroy: true,
reject_if: lambda { |at| at["recipient"].blank? && at["delay"].blank? }
enum kind: [:domain, :ssl]

View file

@ -3,7 +3,7 @@
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer not null
# channel :integer default("email"), not null
# delay :integer not null
# recipient :string(255) not null
# sent_at :datetime
@ -28,7 +28,7 @@ class Notification < ApplicationRecord
enum status: [:pending, :ongoing, :succeed, :failed]
validates :channel, presence: true
validates :delay, presence: true
validates :delay, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates :recipient, presence: true
def pending!

View file

@ -0,0 +1,21 @@
class NotificationPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.joins(:check).where(checks: { user: user })
end
end
def destroy?
check_owner?
end
def show?
false
end
private
def check_owner?
record.check.user == user
end
end

View file

@ -12,5 +12,18 @@
<%= f.input :active %>
<% end %>
<%= f.button :submit, "Validate", class: "btn-primary" %>
<h2 class="mt-5"><%= t(".notifications") %></h2>
<p class="alert alert-light"><%= t(".notifications_hint") %></p>
<%- check.notifications.each_with_index do |notification, index| %>
<div data-notification-id="<%= notification.id %>">
<%= f.fields_for :notifications, notification do |nf| %>
<%= render "notifications/nested_form_headers", f: nf if index.zero? %>
<%= render "notifications/nested_form", f: nf, check: check %>
<% end %>
</div>
<% end %>
<%= f.button :submit, class: "btn-primary mt-5" %>
<% end %>

View file

@ -0,0 +1,29 @@
<fieldset class="form-group">
<div class="form-row">
<%- if many_channels_available? %>
<div class="form-group col-md-2">
<%- if f.object.new_record? -%>
<%= f.input :channel, collection: Notification.channels.keys, label: false %>
<% else -%>
<%= f.input_field :channel, as: :string, readonly: true, class: "form-control-plaintext" %>
<%- end %>
</div>
<% end %>
<div class="form-group <%= recipient_col_class %>">
<%= f.input :recipient, as: :email, label: false %>
</div>
<div class="form-group col-md-2">
<%= f.input :delay, as: :integer, label: false %>
</div>
<div class="form-group col-md-1">
<% if f.object.persisted? %>
<%= link_to check_notification_path(check, f.object), method: :delete, remote: true, class: "btn btn-danger" do %>
<%== Octicons::Octicon.new("x", width: 15, height: 20).to_svg %>
<% end %>
<% end %>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,15 @@
<div class="form-row">
<%- if many_channels_available? %>
<div class="col-md-2">
<%= f.label :channel %>
</div>
<% end %>
<div class="<%= recipient_col_class %>">
<%= f.label :recipient %>
</div>
<div class="col-md-2">
<%= f.label :delay %>
</div>
</div>

View file

@ -0,0 +1 @@
document.querySelector("[data-notification-id='<%= @notification.id %>']").remove();

View file

@ -18,7 +18,7 @@ Rails.application.config.content_security_policy do |policy|
end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# Report CSP violations to a specified URI
# For further information see the following documentation:

View file

@ -17,6 +17,11 @@ en:
new:
tos_acceptance_html: "You must accept our Terms of service"
simple_form:
placeholders:
notifications:
recipient: john@example.org
flashes:
user_not_authorized: "You are not authorized to access to this resource."
@ -44,3 +49,7 @@ en:
You have not set up a check yet.
Please add a <a href="%{new_domain_path}">domain</a>
or a <a href="%{new_ssl_path}">ssl</a> !
form:
notifications_hint: |
Receive notifications to warn you when our system detects that the
expiration date is coming. The delay is set in number of days.

View file

@ -1,6 +1,7 @@
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
# checks GET /checks(.:format) checks#index
# POST /checks(.:format) checks#create
# new_check GET /checks/new(.:format) checks#new
@ -46,7 +47,9 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :checks, except: [:show]
resources :checks, except: [:show] do
resources :notifications, only: [:destroy]
end
devise_for :users
root to: "pages#home"

View file

@ -2,4 +2,9 @@ require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :headless_chrome
def teardown
Capybara.reset_sessions!
Warden.test_reset!
end
end

View file

@ -2,4 +2,22 @@ module ChexpireAssertions
def assert_just_now(expected)
assert_in_delta expected.to_i, Time.now.to_i, 1.0
end
def assert_permit(user, record, action)
msg = "User #{user.inspect} should be permitted to #{action} #{record}, but isn't permitted"
assert policy_permit(user, record, action), msg
end
def refute_permit(user, record, action)
msg = "User #{user.inspect} should NOT be permitted to #{action} #{record}, but is permitted"
refute policy_permit(user, record, action), msg
end
private
def policy_permit(user, record, action)
test_name = self.class.ancestors.select { |a| a.to_s.match(/PolicyTest/) }.first
klass = test_name.to_s.gsub(/Test/, "")
klass.constantize.new(user, record).public_send("#{action}?")
end
end

View file

@ -67,5 +67,11 @@ FactoryBot.define do
trait :inactive do
active false
end
trait :with_notifications do
after :create do |check|
create_list :notification, 2, check: check
end
end
end
end

View file

@ -3,7 +3,7 @@
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer not null
# channel :integer default("email"), not null
# delay :integer not null
# recipient :string(255) not null
# sent_at :datetime

View file

@ -3,7 +3,7 @@
# Table name: notifications
#
# id :bigint(8) not null, primary key
# channel :integer not null
# channel :integer default("email"), not null
# delay :integer not null
# recipient :string(255) not null
# sent_at :datetime

View file

@ -1,13 +1,40 @@
require "test_helper"
class CheckPolicyTest < ActiveSupport::TestCase
def test_scope; end
setup do
@owner, @other = create_list(:user, 2)
@check = create(:check, user: @owner)
end
def test_show; end
test "create" do
assert_permit @other, Check, :create
assert_permit @other, Check, :new
end
def test_create; end
test "check owner" do
assert_permit @owner, @check, :update
assert_permit @owner, @check, :edit
assert_permit @owner, @check, :destroy
assert_permit @owner, @check, :show
end
def test_update; end
test "anonymous and other user" do
refute_permit @other, @check, :update
refute_permit @other, @check, :edit
refute_permit @other, @check, :destroy
refute_permit @other, @check, :show
def test_destroy; end
refute_permit nil, @check, :update
refute_permit nil, @check, :edit
refute_permit nil, @check, :destroy
refute_permit nil, @check, :show
end
test "scope only to owner" do
others = create_list(:check, 2, user: @other)
assert_empty Pundit.policy_scope!(nil, Check)
assert_equal [@check], Pundit.policy_scope!(@owner, Check)
assert_equal others, Pundit.policy_scope!(@other, Check)
end
end

View file

@ -0,0 +1,32 @@
require "test_helper"
class NotificationPolicyTest < ActiveSupport::TestCase
setup do
@owner, @other = create_list(:user, 2)
@notification = create(:notification, check: build(:check, user: @owner))
end
test "permit to check user" do
assert_permit @owner, @notification, :destroy
end
test "disallow to anonymous and other user" do
refute_permit @other, @notification, :destroy
refute_permit nil, @notification, :destroy
end
test "scope only to user checks" do
other_notifications = create_list(:notification, 2, check: build(:check, user: @other))
assert_empty Pundit.policy_scope!(nil, Notification)
assert_equal [@notification], Pundit.policy_scope!(@owner, Notification)
assert_equal other_notifications, Pundit.policy_scope!(@other, Notification)
end
test "disabled actions" do
refute_permit @owner, @notification, :update
refute_permit @owner, @notification, :edit
refute_permit @owner, @notification, :create
refute_permit @owner, @notification, :index
end
end

View file

@ -0,0 +1,85 @@
require "application_system_test_case"
class ChecksTest < ApplicationSystemTestCase
setup do
@user = create(:user)
login_as(@user)
@check = create(:check, :with_notifications, user: @user)
end
test "create a check and a notification" do
visit new_check_path
domain = "domain-test.fr"
fill_in("check[domain]", with: domain)
choose "domain"
recipient = "recipient@example.org"
fill_in("check[notifications_attributes][0][recipient]", with: recipient)
fill_in("check[notifications_attributes][0][delay]", with: 30)
click_button
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
assert page.has_content?(domain)
notification = Notification.last
assert_equal recipient, notification.recipient
assert_equal 30, notification.delay
assert notification.email?
assert notification.pending?
end
test "remove a notification" do
visit edit_check_path(@check)
notification = @check.notifications.first
selector = "[data-notification-id=\"#{notification.id}\"]"
assert_difference "Notification.where(check_id: #{@check.id}).count", -1 do
within selector do
find(".btn-danger").click
end
page.has_no_content?(selector)
end
end
test "update a check" do
visit edit_check_path(@check)
fill_in "check[comment]", with: "My comment"
click_button "Update Check"
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
assert page.has_content?("My comment")
end
test "add a notification" do
visit edit_check_path(@check)
recipient = "recipient2@example.org"
fill_in("check[notifications_attributes][2][recipient]", with: recipient)
fill_in("check[notifications_attributes][2][delay]", with: 55)
assert_difference "Notification.where(check_id: #{@check.id}).count", +1 do
click_button "Update Check"
assert_equal checks_path, page.current_path
end
assert page.has_css?(".alert-success")
notification = Notification.last
assert_equal recipient, notification.recipient
assert_equal 55, notification.delay
assert notification.email?
assert notification.pending?
end
end