21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-05-03 09:20:49 +02:00

Notification template CRUD

This commit is contained in:
Colin Darie 2018-08-30 14:40:47 +02:00
parent cf161e6468
commit a898fadf9c
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
27 changed files with 468 additions and 71 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

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

@ -33,6 +33,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,21 +88,22 @@ 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"
profile: "Profile"
home_header:
home_header:
welcome: "Chexpire"
intro: "Never forget to renew a domain name or SSL certificate."
beta_banner:
beta_banner:
beta_info: "Chexpire is in \"beta\" release: only few TLD (.com/.net/.org/.fr) are verified for domain name checks and TLS 1.2 is not supported for SSL checks."
issue_link: "Please report issues."
pages:
home:
why: "Why Chexpire?"
why: "Why Chexpire?"
description: "Chexpire is a Free Software (AGPLv3 license) to manage the expiration of domain names and SSL certificates. It is primarily an ergonomic web interface that allows you easily to add new domain names/SSL certificates to monitor, and custom/unlimited notifications to be notified before expiration."
centralization: "Centralize all your expiry dates"
centralization-details: "Do you have domain names at different registrars? many Let's Encrypt SSL certificates with automatic renewal? You will enjoy everything centralized in a web interface: adding a domain name/SSL certificate in two clicks, sorted list, search bar etc."
@ -139,3 +160,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

@ -12,8 +12,10 @@ fr:
domain_created_at: "Date de création"
domain_updated_at: "Date de modification"
notification:
label: Étiquette
interval: Délai
recipient: Destinataire
checks_count: Vérifications
user:
tos_accepted: "Conditions d'utilisation"
notifications_enabled: "Notifications activées"
@ -26,6 +28,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:
@ -70,6 +76,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:
@ -100,21 +116,22 @@ 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"
profile: "Profil"
home_header:
home_header:
welcome: "Chexpire"
intro: "vous n'oublierez plus jamais de renouveler un nom de domaine ou un certificat SSL."
beta_banner:
beta_banner:
beta_info: "Chexpire est en version \"beta\" : seuls certains TLD (.com/.net/.org/.fr) sont vérifiés pour les noms de domaine et TLS 1.2 n'est pas supporté pour les vérifications SSL."
issue_link: "Merci de nous reporter bugs et suggestions."
pages:
home:
why: "Pourquoi Chexpire ?"
why: "Pourquoi Chexpire ?"
description: "Chexpire est un Logiciel Libre (licence AGPLv3) permettant de gérer l'expiration de noms de domaine et de certificats SSL. C'est avant tout une interface web ergonomique permettant d'ajouter simplement de nouveaux noms de domaine/certificats SSL à surveiller, et des notifications sur mesure et illimitées pour être averti·e avant expiration."
centralization: "Centralisez toutes vos dates d'expiration"
centralization-details: "Vous avez des noms de domaine chez différents registrars ? de nombreux certificats SSL Let's Encrypt en renouvellement automatique ? Vous allez apprécier de tout centraliser simplement dans une interface web : ajout d'un nom de domaine/certificat SSL en deux clics, liste récapitulative triée, barre de recherche etc."
@ -137,10 +154,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
@ -171,3 +184,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

@ -1,10 +1,7 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# == 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
# checks GET /checks(.:format) checks#index
# POST /checks(.:format) checks#create
# new_check GET /checks/new(.:format) checks#new
@ -12,6 +9,13 @@
# 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
@ -37,7 +41,7 @@
# 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
@ -45,15 +49,20 @@
# letter GET /:id(/:style)(.:format) letter_opener_web/letters#show
# GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
# In order to update the route map above,
# run `bundle exec annotate -r` after modifying this file
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]
resources :check_notifications, only: [:destroy]
end
resources :notifications, except: [:show]
devise_for :users
root to: "pages#home"

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