diff --git a/app/controllers/check_notifications_controller.rb b/app/controllers/check_notifications_controller.rb new file mode 100644 index 0000000..b1e47b1 --- /dev/null +++ b/app/controllers/check_notifications_controller.rb @@ -0,0 +1,47 @@ +# Copyright (C) 2018 Colin Darie , 2018 Evolix +# 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 diff --git a/app/controllers/checks_controller.rb b/app/controllers/checks_controller.rb index 3913cd7..143bf37 100644 --- a/app/controllers/checks_controller.rb +++ b/app/controllers/checks_controller.rb @@ -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 diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 8fc3103..e159174 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -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 diff --git a/app/frontend/scss/components/callouts.scss b/app/frontend/scss/components/callouts.scss new file mode 100644 index 0000000..75f4bb4 --- /dev/null +++ b/app/frontend/scss/components/callouts.scss @@ -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 +} diff --git a/app/frontend/scss/components/notifications.scss b/app/frontend/scss/components/notifications.scss new file mode 100644 index 0000000..aef4306 --- /dev/null +++ b/app/frontend/scss/components/notifications.scss @@ -0,0 +1,8 @@ +// Copyright (C) 2018 Colin Darie , 2018 Evolix +// License: GNU AGPL-3+ (see full text in LICENSE file) + +.notifications-table { + .action a { + color: black; + } +} diff --git a/app/frontend/scss/index.scss b/app/frontend/scss/index.scss index 2b38b06..20ea0ae 100644 --- a/app/frontend/scss/index.scss +++ b/app/frontend/scss/index.scss @@ -5,5 +5,7 @@ @import '~bootstrap/scss/bootstrap'; @import 'layout'; @import 'icons'; +@import 'components/callouts'; @import 'components/users'; @import 'components/checks'; +@import 'components/notifications'; diff --git a/app/helpers/check_notifications_helper.rb b/app/helpers/check_notifications_helper.rb new file mode 100644 index 0000000..a6dbd59 --- /dev/null +++ b/app/helpers/check_notifications_helper.rb @@ -0,0 +1,8 @@ +# Copyright (C) 2018 Colin Darie , 2018 Evolix +# 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 diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 0e9b90d..706cbe4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,12 +1,5 @@ -# Copyright (C) 2018 Colin Darie , 2018 Evolix -# 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 diff --git a/app/models/notification.rb b/app/models/notification.rb index 2c39b0e..b49f6d6 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -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] diff --git a/app/policies/check_notification_policy.rb b/app/policies/check_notification_policy.rb new file mode 100644 index 0000000..ff9837a --- /dev/null +++ b/app/policies/check_notification_policy.rb @@ -0,0 +1,24 @@ +# Copyright (C) 2018 Colin Darie , 2018 Evolix +# 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 diff --git a/app/policies/notification_policy.rb b/app/policies/notification_policy.rb index ba2a514..c541694 100644 --- a/app/policies/notification_policy.rb +++ b/app/policies/notification_policy.rb @@ -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 diff --git a/app/views/notifications/_nested_form.html.erb b/app/views/check_notifications/_nested_form.html.erb similarity index 100% rename from app/views/notifications/_nested_form.html.erb rename to app/views/check_notifications/_nested_form.html.erb diff --git a/app/views/notifications/_nested_form_headers.html.erb b/app/views/check_notifications/_nested_form_headers.html.erb similarity index 100% rename from app/views/notifications/_nested_form_headers.html.erb rename to app/views/check_notifications/_nested_form_headers.html.erb diff --git a/app/views/notifications/destroy.js.erb b/app/views/check_notifications/destroy.js.erb similarity index 100% rename from app/views/notifications/destroy.js.erb rename to app/views/check_notifications/destroy.js.erb diff --git a/app/views/checks/_form.html.erb b/app/views/checks/_form.html.erb index 991494f..c01f873 100644 --- a/app/views/checks/_form.html.erb +++ b/app/views/checks/_form.html.erb @@ -53,6 +53,5 @@ <% end %> - <%= f.button :submit, class: "btn-primary mt-5" %> <% end %> diff --git a/app/views/checks/_table.html.erb b/app/views/checks/_table.html.erb index cba39e9..8cb7244 100644 --- a/app/views/checks/_table.html.erb +++ b/app/views/checks/_table.html.erb @@ -7,16 +7,20 @@ <%= t(".th.domain") %> - - <%== checks_sort_links(:domain) %> - + <% unless defined?(skip_sort) %> + + <%== checks_sort_links(:domain) %> + + <% end %> <%= t(".th.expiry_date") %> <%= t(".th.expiry_date_short") %> - - <%== checks_sort_links(:domain_expires_at) %> - + <% unless defined?(skip_sort) %> + + <%== checks_sort_links(:domain_expires_at) %> + + <% end %> <%= t(".th.edit") %> @@ -48,4 +52,4 @@ -<%= paginate @checks %> +<%= paginate checks unless defined?(skip_pagination)%> diff --git a/app/views/notifications/_form.html.erb b/app/views/notifications/_form.html.erb new file mode 100644 index 0000000..54ec17a --- /dev/null +++ b/app/views/notifications/_form.html.erb @@ -0,0 +1,13 @@ +<% # Copyright (C) 2018 Colin Darie , 2018 Evolix %> +<% # 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 %> diff --git a/app/views/notifications/_table.html.erb b/app/views/notifications/_table.html.erb new file mode 100644 index 0000000..f54721d --- /dev/null +++ b/app/views/notifications/_table.html.erb @@ -0,0 +1,46 @@ +<% # Copyright (C) 2018 Colin Darie , 2018 Evolix %> +<% # License: GNU AGPL-3+ (see full text in LICENSE file) %> +
+ + + + + + + + + + + + <% notifications.each do |notification| %> + + + + + + + + <% end %> + +
+ <%= Notification.human_attribute_name("label") %> + + <%= Notification.human_attribute_name("recipient") %> + + <%= Notification.human_attribute_name("interval") %> + + <%= Notification.human_attribute_name("checks_count") %> + <%= t(".th.edit") %>
+ <%= notification.label %> + + <%= notification.recipient %> + + <%= t(".interval_in_days", count: notification.interval) %> + + <%= notification.checks_count %> + + <%= link_to edit_notification_path(notification), "data-turbolinks" => false do %> + <%== Octicons::Octicon.new("pencil").to_svg %> + <% end %> +
+
diff --git a/app/views/notifications/edit.html.erb b/app/views/notifications/edit.html.erb new file mode 100644 index 0000000..9c996c5 --- /dev/null +++ b/app/views/notifications/edit.html.erb @@ -0,0 +1,27 @@ +<% # Copyright (C) 2018 Colin Darie , 2018 Evolix %> +<% # License: GNU AGPL-3+ (see full text in LICENSE file) %> +
+
+
+

<%= t(".title") %>

+ + <%= render "form", notification: @notification %> +
+
+ +
+
+

<%= t(".checks", count: @notification.checks_count) %>

+ <% if @notification.checks_count.positive? %> + <%= render "checks/table", checks: @notification.checks, skip_sort: true, skip_pagination: true %> + <% end %> +
+
+ +
+
+ <%= 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) }) %> +
+
+
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 0000000..af5f65b --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,22 @@ +<% # Copyright (C) 2018 Colin Darie , 2018 Evolix %> +<% # License: GNU AGPL-3+ (see full text in LICENSE file) %> +
+
+
+ <% if @notifications.empty? %> +
+ <%= t(".no_notification_yet_html", new_path: new_notification_path) %> +
+ <% else %> +
+ <%= link_to("Ajouter une notification", new_notification_path, class: "btn btn-primary") %> +
+

<%= t(".title") %>

+ <%= render "table", notifications: @notifications %> + <% end %> +
+

<%= t(".explanation") %>

+
+
+
+
diff --git a/app/views/notifications/new.html.erb b/app/views/notifications/new.html.erb new file mode 100644 index 0000000..6c777a9 --- /dev/null +++ b/app/views/notifications/new.html.erb @@ -0,0 +1,11 @@ +<% # Copyright (C) 2018 Colin Darie , 2018 Evolix %> +<% # License: GNU AGPL-3+ (see full text in LICENSE file) %> +
+
+
+

<%= t(".title") %>

+ + <%= render "form", notification: @notification %> +
+
+
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 00b87e5..4da000b 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -24,6 +24,9 @@ + <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 58c4495..5eb7edf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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. + Create your first notification. + 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. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e48ca32..499ac99 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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. + Créez votre première notification. + 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. diff --git a/config/routes.rb b/config/routes.rb index 6dc56fd..ee3120f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/test/controllers/check_notifications_controller_test.rb b/test/controllers/check_notifications_controller_test.rb new file mode 100644 index 0000000..57b93cf --- /dev/null +++ b/test/controllers/check_notifications_controller_test.rb @@ -0,0 +1,10 @@ +# Copyright (C) 2018 Colin Darie , 2018 Evolix +# 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 diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb index c3ba49f..d608893 100644 --- a/test/controllers/notifications_controller_test.rb +++ b/test/controllers/notifications_controller_test.rb @@ -1,7 +1,4 @@ -# Copyright (C) 2018 Colin Darie , 2018 Evolix -# License: GNU AGPL-3+ (see full text in LICENSE file) - -require "test_helper" +require 'test_helper' class NotificationsControllerTest < ActionDispatch::IntegrationTest # test "the truth" do