21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-25 21:40:49 +02:00

Notifications template CRUD

This commit is contained in:
Colin Darie 2018-08-31 10:06:16 +02:00
parent 0052b54967
commit 9c35dbc7a6
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
27 changed files with 456 additions and 62 deletions

View file

@ -0,0 +1,47 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class CheckNotificationsController < 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, :interval)
end
def check_path
edit_check_path(check_id: params[:check_id])
end
end

View file

@ -35,10 +35,10 @@ class ChecksController < ApplicationController
authorize @check
if @check.save
flash[:notice] = t(".saved")
flash[:notice] = t("checks.created", scope: :flashes)
redirect_to checks_path
else
flash.now[:alert] = t(".invalid")
flash.now[:alert] = t("checks.invalid", scope: :flashes)
render :new
end
end
@ -49,10 +49,10 @@ class ChecksController < ApplicationController
def update
if @check.update(update_check_params)
flash[:notice] = "Your check has been updated."
flash[:notice] = t("checks.updated", scope: :flashes)
redirect_to checks_path
else
flash.now[:alert] = "An error occured."
flash.now[:alert] = t("checks.invalid", scope: :flashes)
build_empty_notification
render :edit
end
@ -61,7 +61,7 @@ class ChecksController < ApplicationController
def destroy
@check.destroy!
flash[:notice] = "Your check has been destroyed."
flash[:notice] = t("checks.destroyed", scope: :flashes)
redirect_to checks_path
end

View file

@ -3,45 +3,62 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!
before_action :set_notification, except: [:create]
before_action :set_notification, except: [:index, :new, :create]
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@notifications = policy_scope(Notification).order(checks_count: :desc)
end
def new;
@notification = Notification.new
authorize @notification
@notification.recipient = current_user.email
end
def create
check = Check.find(params[:check_id])
@notification = check.notifications.build(notification_params)
@notification = Notification.new(notification_params)
@notification.user = current_user
authorize @notification
if @notification.save
flash[:notice] = "Your notification has been saved."
redirect_to check_path
flash[:notice] = t("notifications.created", scope: :flashes)
redirect_to notifications_path
else
flash.now[:alert] = "An error occured."
render "checks/edit"
flash.now[:alert] = t("notifications.invalid", scope: :flashes)
render :new
end
end
def edit; end
def update
if @notification.update(notification_params)
flash[:notice] = t("notifications.updated", scope: :flashes)
redirect_to notifications_path
else
flash.now[:alert] = t("notifications.error", scope: :flashes)
render :edit
end
end
def destroy
@notification.destroy!
respond_to do |format|
format.js
end
flash[:notice] = t("notifications.destroyed", scope: :flashes)
redirect_to notifications_path
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])
@notification = Notification.find(params[:id])
authorize @notification
end
def notification_params
params.require(:notification).permit(:channel, :recipient, :interval)
end
def check_path
edit_check_path(check_id: params[:check_id])
params.require(:notification).permit(:label, :recipient, :interval)
end
end

View file

@ -0,0 +1,51 @@
// Taken from Bootstrap 4 documentation
.bd-callout {
padding: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #eee;
border-left-width: .25rem;
border-radius: .25rem
}
.bd-callout h4 {
margin-top: 0;
margin-bottom: .25rem
}
.bd-callout p:last-child {
margin-bottom: 0
}
.bd-callout code {
border-radius: .25rem
}
.bd-callout+.bd-callout {
margin-top: -.25rem
}
.bd-callout-info {
border-left-color: #5bc0de
}
.bd-callout-info h4 {
color: #5bc0de
}
.bd-callout-warning {
border-left-color: #f0ad4e
}
.bd-callout-warning h4 {
color: #f0ad4e
}
.bd-callout-danger {
border-left-color: #d9534f
}
.bd-callout-danger h4 {
color: #d9534f
}

View file

@ -0,0 +1,8 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file)
.notifications-table {
.action a {
color: black;
}
}

View file

@ -5,5 +5,7 @@
@import '~bootstrap/scss/bootstrap';
@import 'layout';
@import 'icons';
@import 'components/callouts';
@import 'components/users';
@import 'components/checks';
@import 'components/notifications';

View file

@ -0,0 +1,8 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
module CheckNotificationsHelper
def recipient_col_class
many_channels_available? ? "col-md-7" : "col-md-9"
end
end

View file

@ -1,12 +1,5 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
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

@ -28,8 +28,8 @@
class Notification < ApplicationRecord
belongs_to :user
has_many :check_notifications
has_many :checks, through: :notifications
has_many :check_notifications, dependent: :destroy
has_many :checks, -> { order(domain_expires_at: :asc) }, through: :check_notifications
enum channel: [:email]

View file

@ -0,0 +1,24 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
class CheckNotificationPolicy < 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

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

View file

@ -53,6 +53,5 @@
</div>
<% end %>
<%= f.button :submit, class: "btn-primary mt-5" %>
<% end %>

View file

@ -7,16 +7,20 @@
<th scope="col"></th>
<th scope="col">
<%= t(".th.domain") %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain) %>
</span>
<% unless defined?(skip_sort) %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain) %>
</span>
<% end %>
</th>
<th scope="col">
<span class="d-none d-sm-inline"><%= t(".th.expiry_date") %></span>
<span class="d-inline d-sm-none"><%= t(".th.expiry_date_short") %></span>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain_expires_at) %>
</span>
<% unless defined?(skip_sort) %>
<span class="sort-links mx-sm-2 text-nowrap">
<%== checks_sort_links(:domain_expires_at) %>
</span>
<% end %>
</th>
<th scope="col" class="text-right"><%= t(".th.edit") %></th>
</tr>
@ -48,4 +52,4 @@
</table>
</div>
<%= paginate @checks %>
<%= paginate checks unless defined?(skip_pagination)%>

View file

@ -0,0 +1,13 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<%= simple_form_for(notification) do |f| %>
<%= f.input :label, hint: t(".label_hint")%>
<%= f.input :recipient, as: :email,
input_html: { autocapitalize: :none, autocorrect: :off }
%>
<%= f.input :interval, as: :integer, required: true %>
<%= f.button :submit, class: "btn-primary mt-3" %>
<% end %>

View file

@ -0,0 +1,46 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="mb-4 table-responsive">
<table class="table notifications-table">
<thead>
<tr>
<th scope="col">
<%= Notification.human_attribute_name("label") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("recipient") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("interval") %>
</th>
<th scope="col">
<%= Notification.human_attribute_name("checks_count") %>
</th>
<th scope="col" class="text-right"><%= t(".th.edit") %></th>
</tr>
</thead>
<tbody>
<% notifications.each do |notification| %>
<tr class="notification-row">
<td>
<strong><%= notification.label %></strong>
</td>
<td>
<%= notification.recipient %>
</td>
<td>
<%= t(".interval_in_days", count: notification.interval) %>
</td>
<td>
<%= notification.checks_count %>
</td>
<td class="action text-right">
<%= link_to edit_notification_path(notification), "data-turbolinks" => false do %>
<%== Octicons::Octicon.new("pencil").to_svg %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

View file

@ -0,0 +1,27 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1><%= t(".title") %></h1>
<%= render "form", notification: @notification %>
</div>
</div>
<div class="row mt-5 justify-content-center">
<div class="col-12 col-lg-10">
<h4><%= t(".checks", count: @notification.checks_count) %></h4>
<% if @notification.checks_count.positive? %>
<%= render "checks/table", checks: @notification.checks, skip_sort: true, skip_pagination: true %>
<% end %>
</div>
</div>
<div class="row mt-5 mb-3 justify-content-center">
<div class="col-12 col-lg-10">
<%= button_to(t("helpers.submit.notification.delete"), notification_path(@notification), class: "btn btn-danger", method: :delete,
data: { confirm: t(".destroy_confirmation", count: @notification.checks_count) }) %>
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-9">
<% if @notifications.empty? %>
<div class="alert alert-info">
<%= t(".no_notification_yet_html", new_path: new_notification_path) %>
</div>
<% else %>
<div class="row justify-content-md-end">
<%= link_to("Ajouter une notification", new_notification_path, class: "btn btn-primary") %>
</div>
<h1 class="mb-3 mb-sm-5"><%= t(".title") %></h1>
<%= render "table", notifications: @notifications %>
<% end %>
<div class="bd-callout bd-callout-info">
<p><%= t(".explanation") %></p>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<% # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> %>
<% # License: GNU AGPL-3+ (see full text in LICENSE file) %>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1><%= t(".title") %></h1>
<%= render "form", notification: @notification %>
</div>
</div>
</div>

View file

@ -24,6 +24,9 @@
<li class="nav-item">
<%= link_to(t(".GitHub"), "https://github.com/Evolix/chexpire", class: "nav-link") %>
</li>
<li class="nav-item">
<%= link_to(t(".my_notifications"), notifications_path, class: "nav-link") %>
</li>
<% end %>
</ul>

View file

@ -33,8 +33,28 @@ en:
notifications:
recipient: john@example.com
helpers:
submit:
check:
create: "Create"
update: "Update"
notification:
create: "Create"
update: "Update"
delete: "Delete"
flashes:
user_not_authorized: "You are not authorized to access to this resource."
checks:
create: The check has been saved.
updated: The check has been updated.
invalid: Please check the form.
destroyed: The check has been destroyed.
notifications:
created: The notification has been created.
updated: The notification has been updated."
destroyed: The notification has been destroyed.
invalid: Please check the form.
notifications_mailer:
domain_expires_soon:
@ -68,6 +88,7 @@ en:
my_checks: "My checks"
new_domain_check: "New domain check"
new_ssl_check: "New SSL check"
my_notifications: "My notifications"
sign_up: "Sign up"
sign_in: "Log in"
sign_out: "Log out"
@ -143,3 +164,36 @@ en:
zero: "Last check successful: today"
one: "Last check successful: yesterday"
other: "Last check successful %{count} days ago"
notifications:
index:
title: "List of your notifications"
no_notification_yet_html: |
You have not set up a notification yet.
<a href="%{new_path}">Create your first notification</a>.
explanation: |
For each of your checks,
you can associate one or more notifications that you will receive
by email at the interval of your choice before the expiration date.
table:
th:
edit: Edit
interval_in_days:
one: 1 day
other: "%{count} days"
edit:
title: "Edit the notification"
checks:
zero: No check associated check.
one: Check associated
other: Checks associated
destroy_confirmation:
zero: "Are you sure ?"
one: |
Are you sure ?
You won't receive this notification for the associated check.
other: |
Are you sure ?
You won't receive this notification for the associated checks.
form:
label_hint: This label allows you to identify this notification and associate it with a check.

View file

@ -13,8 +13,10 @@ fr:
domain_updated_at: "Date de modification"
domain_expires_at: "Date d'expiration"
notification:
label: Étiquette
interval: Délai
recipient: Destinataire
checks_count: Vérifications
user:
tos_accepted: "Conditions d'utilisation"
notifications_enabled: "Notifications activées"
@ -27,6 +29,10 @@ fr:
check:
create: "Créer"
update: "Valider"
notification:
create: "Créer"
update: "Modifier"
delete: "Supprimer la notification"
page_entries_info:
one_page:
display_entries:
@ -71,6 +77,16 @@ fr:
flashes:
user_not_authorized: "Vous n'êtes pas autorisé•e à accéder à cette ressouce."
checks:
create: La vérification est enregistrée.
updated: La vérification est mise à jour.
invalid: Veuillez vérifier le formulaire.
destroyed: La vérification est supprimée.
notifications:
created: La notification est créée.
updated: "La notification est mise à jour."
destroyed: La notification est supprimée.
invalid: Veuillez vérifier le formulaire.
notifications_mailer:
domain_expires_soon:
@ -101,6 +117,7 @@ fr:
my_checks: "Mes vérifications"
new_domain_check: "Nouveau nom de domaine"
new_ssl_check: "Nouveau certificat SSL"
my_notifications: "Mes notifications"
sign_up: "Enregistrement"
sign_in: "Connexion"
sign_out: "Déconnexion"
@ -138,10 +155,6 @@ fr:
ssl:
title: Nouvelle vérification d'un certificat SSL
create:
saved: La vérification est enregistrée.
invalid: Veuillez vérifier le formulaire.
filters:
kind_domain: Domaine
kind_ssl: SSL
@ -176,3 +189,38 @@ fr:
zero: "Dernière vérification réussie : aujourd'hui"
one: "Dernière vérification réussie : hier"
other: "Dernière vérification réussie il y a %{count} jours"
notifications:
index:
title: "Liste de vos notifications"
no_notification_yet_html: |
Vous n'avez pas encore créé de notification.
<a href="%{new_domain_path}">Créez votre première notification</a>.
explanation: |
Pour chacune de vos vérifications,
vous pouvez associer une ou plusieurs notifications que vous recevrez
par email un certain nombre de jours avant la date d'expiration.
table:
th:
edit: Modifier
interval_in_days:
one: 1 jour
other: "%{count} jours"
new:
title: Nouvelle notification
edit:
title: "Modification d'une notification"
checks:
zero: Aucune vérification associée.
one: Vérification associée
other: Vérifications associées
destroy_confirmation:
zero: "Confirmez-vous la suppression de cette notification ?"
one: |
Confirmez-vous la suppression ?
Vous ne recevrez plus la notification associée à cette vérification.
other: |
Confirmez-vous la suppression ?
Vous ne recevrez plus les notifications associées à ces vérifications.
form:
label_hint: Ce nom vous permet d'identifier cette notification pour l'associer à une vérification.

View file

@ -7,12 +7,11 @@ 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] do
resources :notifications, only: [:destroy]
collection do
post :supports, format: :json
end
resources :check_notifications, only: [:destroy]
end
resources :notifications, except: [:show]
devise_for :users
root to: "pages#home"
@ -22,7 +21,7 @@ end
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
# check_check_notification DELETE /checks/:check_id/check_notifications/:id(.:format) check_notifications#destroy
# supports_checks POST /checks/supports(.:format) checks#supports
# checks GET /checks(.:format) checks#index
# POST /checks(.:format) checks#create
@ -31,6 +30,13 @@ end
# check PATCH /checks/:id(.:format) checks#update
# PUT /checks/:id(.:format) checks#update
# DELETE /checks/:id(.:format) checks#destroy
# notifications GET /notifications(.:format) notifications#index
# POST /notifications(.:format) notifications#create
# new_notification GET /notifications/new(.:format) notifications#new
# edit_notification GET /notifications/:id/edit(.:format) notifications#edit
# notification PATCH /notifications/:id(.:format) notifications#update
# PUT /notifications/:id(.:format) notifications#update
# DELETE /notifications/:id(.:format) notifications#destroy
# new_user_session GET /users/sign_in(.:format) devise/sessions#new
# user_session POST /users/sign_in(.:format) devise/sessions#create
# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
@ -56,7 +62,7 @@ end
# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
#
#
# Routes for LetterOpenerWeb::Engine:
# clear_letters DELETE /clear(.:format) letter_opener_web/letters#clear
# delete_letter DELETE /:id(.:format) letter_opener_web/letters#destroy

View file

@ -0,0 +1,10 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
require "test_helper"
class CheckNotificationsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +1,4 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
require "test_helper"
require 'test_helper'
class NotificationsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do