From 10473182d456f0484e159ebae32e6244d08b968f Mon Sep 17 00:00:00 2001 From: Karen Date: Thu, 2 Feb 2023 13:37:09 +0100 Subject: [PATCH] (feat) Admin can control its notifications preferences --- .../notification_preferences_controller.rb | 50 +++++++++++ .../api/notification_types_controller.rb | 14 +++ .../javascript/api/notification_preference.ts | 20 +++++ .../src/javascript/api/notification_types.ts | 11 +++ .../notifications/notification-form.tsx | 65 ++++++++++++++ .../notifications/notifications-category.tsx | 58 +++++++++++++ .../notifications/notifications-center.tsx | 21 ++--- .../notifications/notifications-settings.tsx | 86 +++++++++++++++++++ .../models/notification-preference.ts | 24 ++++++ app/frontend/src/stylesheets/application.scss | 4 +- .../notifications/notification-form.scss | 32 +++++++ .../notifications/notifications-category.scss | 52 +++++++++++ app/models/notification_preference.rb | 6 ++ .../notification_preference_policy.rb | 12 +++ .../index.json.jbuilder | 3 + .../show.json.jbuilder | 1 + .../notification_types/index.json.jbuilder | 3 + config/locales/app.admin.en.yml | 3 - config/locales/app.logged.en.yml | 60 ++++++++++++- config/routes.rb | 4 + ...7100506_create_notification_preferences.rb | 4 +- 21 files changed, 517 insertions(+), 16 deletions(-) create mode 100644 app/controllers/api/notification_preferences_controller.rb create mode 100644 app/controllers/api/notification_types_controller.rb create mode 100644 app/frontend/src/javascript/api/notification_preference.ts create mode 100644 app/frontend/src/javascript/api/notification_types.ts create mode 100644 app/frontend/src/javascript/components/notifications/notification-form.tsx create mode 100644 app/frontend/src/javascript/components/notifications/notifications-category.tsx create mode 100644 app/frontend/src/javascript/components/notifications/notifications-settings.tsx create mode 100644 app/frontend/src/javascript/models/notification-preference.ts create mode 100644 app/frontend/src/stylesheets/modules/notifications/notification-form.scss create mode 100644 app/frontend/src/stylesheets/modules/notifications/notifications-category.scss create mode 100644 app/policies/notification_preference_policy.rb create mode 100644 app/views/api/notification_preferences/index.json.jbuilder create mode 100644 app/views/api/notification_preferences/show.json.jbuilder create mode 100644 app/views/api/notification_types/index.json.jbuilder diff --git a/app/controllers/api/notification_preferences_controller.rb b/app/controllers/api/notification_preferences_controller.rb new file mode 100644 index 000000000..0137726d5 --- /dev/null +++ b/app/controllers/api/notification_preferences_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# API Controller for resources of type Notification Preferences +class API::NotificationPreferencesController < API::ApiController + before_action :authenticate_user! + + def index + @notification_preferences = current_user.notification_preferences + end + + # Currently only available for Admin in NotificationPreferencePolicy + def update + authorize NotificationPreference + notification_type = NotificationType.find_by(name: params[:notification_preference][:notification_type]) + @notification_preference = NotificationPreference.find_or_create_by(notification_type: notification_type, user: current_user) + @notification_preference.update(notification_preference_params) + + if @notification_preference.save + render :show, status: :ok + else + render json: @notification_preference.errors, status: :unprocessable_entity + end + end + + # Currently only available for Admin in NotificationPreferencePolicy + def bulk_update + authorize NotificationPreference + errors = [] + params[:notification_preferences].each do |notification_preference| + notification_type = NotificationType.find_by(name: notification_preference[:notification_type]) + db_notification_preference = NotificationPreference.find_or_create_by(notification_type_id: notification_type.id, user: current_user) + + next if db_notification_preference.update(email: notification_preference[:email], in_system: notification_preference[:in_system]) + + errors.push(db_notification_preference.errors) + end + + if errors.any? + render json: errors, status: :unprocessable_entity + else + head :no_content, status: :ok + end + end + + private + + def notification_preference_params + params.require(:notification_preference).permit(:notification_type_id, :in_system, :email) + end +end diff --git a/app/controllers/api/notification_types_controller.rb b/app/controllers/api/notification_types_controller.rb new file mode 100644 index 000000000..e4fbcc102 --- /dev/null +++ b/app/controllers/api/notification_types_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# API Controller for resources of type Notification Types +class API::NotificationTypesController < API::ApiController + before_action :authenticate_user! + + def index + @notification_types = if params[:is_configurable] == 'true' + NotificationType.where(is_configurable: true) + else + NotificationType.all + end + end +end diff --git a/app/frontend/src/javascript/api/notification_preference.ts b/app/frontend/src/javascript/api/notification_preference.ts new file mode 100644 index 000000000..84a91e716 --- /dev/null +++ b/app/frontend/src/javascript/api/notification_preference.ts @@ -0,0 +1,20 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { NotificationPreference } from '../models/notification-preference'; + +export default class NotificationPreferencesAPI { + static async index (): Promise> { + const res: AxiosResponse> = await apiClient.get('/api/notification_preferences'); + return res?.data; + } + + static async update (updatedPreference: NotificationPreference): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/notification_preferences/${updatedPreference.notification_type}`, { notification_preference: updatedPreference }); + return res?.data; + } + + static async bulk_update (updatedPreferences: Array): Promise { + const res: AxiosResponse = await apiClient.patch('/api/notification_preferences/bulk_update', { notification_preferences: updatedPreferences }); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/api/notification_types.ts b/app/frontend/src/javascript/api/notification_types.ts new file mode 100644 index 000000000..4ff40150a --- /dev/null +++ b/app/frontend/src/javascript/api/notification_types.ts @@ -0,0 +1,11 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { NotificationTypeIndexFilter, NotificationType } from '../models/notification-type'; +import ApiLib from '../lib/api'; + +export default class NotificationTypesAPI { + static async index (isConfigurable?:NotificationTypeIndexFilter): Promise> { + const res: AxiosResponse> = await apiClient.get(`/api/notification_types${ApiLib.filtersToQuery(isConfigurable)}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/notifications/notification-form.tsx b/app/frontend/src/javascript/components/notifications/notification-form.tsx new file mode 100644 index 000000000..dc6ad7ffd --- /dev/null +++ b/app/frontend/src/javascript/components/notifications/notification-form.tsx @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; +import { Loader } from '../base/loader'; +import { useTranslation } from 'react-i18next'; +import { NotificationPreference } from '../../models/notification-preference'; +import { useForm } from 'react-hook-form'; +import { FormSwitch } from '../form/form-switch'; +import NotificationPreferencesAPI from '../../api/notification_preference'; + +interface NotificationFormProps { + onError: (message: string) => void, + preference: NotificationPreference +} + +/** + * Displays the list of notifications + */ +const NotificationForm: React.FC = ({ preference, onError }) => { + const { t } = useTranslation('logged'); + const { handleSubmit, formState, control, reset } = useForm({ defaultValues: { ...preference } }); + + // Create or Update (if id exists) a Notification Preference + const onSubmit = (updatedPreference: NotificationPreference) => NotificationPreferencesAPI.update(updatedPreference).catch(onError); + + // Calls submit handler on every change of a Form Switch + const handleChange = () => handleSubmit(onSubmit)(); + + // Resets form on component mount, and if preference changes (happens when bulk updating a category) + useEffect(() => { + reset(preference); + }, [preference]); + + return ( +
+

{t(`app.logged.notification_form.${preference.notification_type}`)}

+
+ + +
+
+ ); +}; + +const NotificationFormWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export { NotificationFormWrapper as NotificationForm }; diff --git a/app/frontend/src/javascript/components/notifications/notifications-category.tsx b/app/frontend/src/javascript/components/notifications/notifications-category.tsx new file mode 100644 index 000000000..6ff31c300 --- /dev/null +++ b/app/frontend/src/javascript/components/notifications/notifications-category.tsx @@ -0,0 +1,58 @@ +import { Loader } from '../base/loader'; +import { useTranslation } from 'react-i18next'; +import { NotificationPreference } from '../../models/notification-preference'; +import { NotificationForm } from './notification-form'; +import { FabButton } from '../base/fab-button'; +import NotificationPreferencesAPI from '../../api/notification_preference'; + +interface NotificationsCategoryProps { + onError: (message: string) => void, + refreshSettings: () => void, + categoryName: string, + preferences: Array +} + +/** + * Displays the list of notifications + */ +const NotificationsCategory: React.FC = ({ onError, categoryName, preferences, refreshSettings }) => { + const { t } = useTranslation('logged'); + // Triggers a general update to enable all notifications for this category + const enableAll = () => updateAll(true); + + // Triggers a general update to disable all notifications for this category + const disableAll = () => updateAll(false); + + // Update all notifications for this category with a bulk_update. + // This triggers a refresh of all the forms. + const updateAll = async (value: boolean) => { + const updatedPreferences: Array = preferences.map(preference => { + return { id: preference.id, notification_type: preference.notification_type, in_system: value, email: value }; + }); + await NotificationPreferencesAPI.bulk_update(updatedPreferences).catch(onError); + refreshSettings(); + }; + + return ( +
+

{`${t(`app.logged.notifications_category.${categoryName}`)}, ${t('app.logged.notifications_category.notify_me_when')}`}

+
+
+ {t('app.logged.notifications_category.enable_all')} + {t('app.logged.notifications_category.disable_all')} +
+ {preferences.map(preference => )} +
+
+ ); +}; + +const NotificationsCategoryWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export { NotificationsCategoryWrapper as NotificationsCategory }; diff --git a/app/frontend/src/javascript/components/notifications/notifications-center.tsx b/app/frontend/src/javascript/components/notifications/notifications-center.tsx index 60424904e..79fe6128b 100644 --- a/app/frontend/src/javascript/components/notifications/notifications-center.tsx +++ b/app/frontend/src/javascript/components/notifications/notifications-center.tsx @@ -4,8 +4,10 @@ import { react2angular } from 'react2angular'; import { Loader } from '../base/loader'; import { FabTabs } from '../base/fab-tabs'; import { NotificationsList } from './notifications-list'; +import { NotificationsSettings } from './notifications-settings'; import { useTranslation } from 'react-i18next'; import MemberAPI from '../../api/member'; +import { UserRole } from '../../models/user'; declare const Application: IApplication; @@ -17,31 +19,30 @@ interface NotificationsCenterProps { * This Admin component groups two tabs : a list of notifications and the notifications settings */ export const NotificationsCenter: React.FC = ({ onError }) => { - const { t } = useTranslation('admin'); - const [isAdmin, setIsAdmin] = useState(false); + const { t } = useTranslation('logged'); + const [role, setRole] = useState(); useEffect(() => { MemberAPI.current() - .then(data => { - if (data.role === 'admin') setIsAdmin(true); - }); + .then(data => setRole(data.role)) + .catch(onError); }, []); return ( <> - {isAdmin && }, { id: 'notifications-list', - title: t('app.admin.notifications_center.notifications_list'), + title: t('app.logged.notifications_center.notifications_list'), content: } ]} />} - {!isAdmin && } + {role !== 'admin' && } ); }; diff --git a/app/frontend/src/javascript/components/notifications/notifications-settings.tsx b/app/frontend/src/javascript/components/notifications/notifications-settings.tsx new file mode 100644 index 000000000..0e5a0ed4c --- /dev/null +++ b/app/frontend/src/javascript/components/notifications/notifications-settings.tsx @@ -0,0 +1,86 @@ +import { Loader } from '../base/loader'; +import { useEffect, useState } from 'react'; +import NotificationPreferencesAPI from '../../api/notification_preference'; +import { NotificationPreference, NotificationCategoryNames, NotificationPreferencesByCategories } from '../../models/notification-preference'; +import { NotificationsCategory } from './notifications-category'; +import NotificationTypesAPI from '../../api/notification_types'; + +interface NotificationsSettingsProps { + onError: (message: string) => void +} + +/** + * Displays the list of notifications + */ +const NotificationsSettings: React.FC = ({ onError }) => { + const [preferencesByCategories, setPreferencesCategories] = useState({}); + + // From a default pattern of categories, and existing preferences and types retrieved from API, + // this function builds an object with Notification Preferences sorted by categories. + const fetchNotificationPreferences = async () => { + let notificationPreferences: Array; + + await NotificationPreferencesAPI.index() + .then(userNotificationPreferences => { + notificationPreferences = userNotificationPreferences; + }) + .catch(onError); + + NotificationTypesAPI.index({ is_configurable: true }) + .then(notificationTypes => { + // Initialize an object with every categories as keys + const newPreferencesByCategories: NotificationPreferencesByCategories = {}; + for (const categoryName of NotificationCategoryNames) { + newPreferencesByCategories[categoryName] = []; + } + + // For every notification type, we check if a notification preference already exists. + // If there is none, we create one with default values. + // Each Notification Preference is then placed in the right category. + notificationTypes.forEach((notificationType) => { + const existingPreference = notificationPreferences.find((notificationPreference) => { + return notificationPreference.notification_type === notificationType.name; + }); + newPreferencesByCategories[notificationType.category].push( + existingPreference || + { + notification_type: notificationType.name, + in_system: true, + email: true + } + ); + }); + setPreferencesCategories(newPreferencesByCategories); + }) + .catch(onError); + }; + + // Triggers the fetch Notification Preferences on component mount + useEffect(() => { + fetchNotificationPreferences(); + }, []); + + return ( +
+ {Object.entries(preferencesByCategories).map((notificationPreferencesCategory) => ( + + )) + } +
+ ); +}; + +const NotificationsSettingsWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export { NotificationsSettingsWrapper as NotificationsSettings }; diff --git a/app/frontend/src/javascript/models/notification-preference.ts b/app/frontend/src/javascript/models/notification-preference.ts new file mode 100644 index 000000000..2e243c4e1 --- /dev/null +++ b/app/frontend/src/javascript/models/notification-preference.ts @@ -0,0 +1,24 @@ +export interface NotificationPreference { + id: number, + notification_type: string, + email: boolean, + in_system: boolean +} + +// This controls the order of the categories' display in the notification center +export const NotificationCategoryNames = [ + 'users_accounts', + 'proof_of_identity', + 'agenda', + 'subscriptions', + 'payments', + 'wallet', + 'shop', + 'projects', + 'accountings', + 'trainings' +] as const; + +export type NotificationCategoryName = typeof NotificationCategoryNames[number]; + +export type NotificationPreferencesByCategories = Record> | Record diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 8e3bf4dde..22fc9555c 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -75,7 +75,9 @@ @import "modules/machines/machines-settings"; @import "modules/machines/required-training-modal"; @import "modules/notifications/notifications-list"; -@import "modules/notifications/notification-line"; +@import "modules/notifications/notification-inline"; +@import "modules/notifications/notifications-category"; +@import "modules/notifications/notification-form"; @import "modules/payment-schedule/payment-schedule-dashboard"; @import "modules/payment-schedule/payment-schedule-summary"; @import "modules/payment-schedule/payment-schedules-list"; diff --git a/app/frontend/src/stylesheets/modules/notifications/notification-form.scss b/app/frontend/src/stylesheets/modules/notifications/notification-form.scss new file mode 100644 index 000000000..a38938d3b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/notifications/notification-form.scss @@ -0,0 +1,32 @@ +.notification-form { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 2rem; + border-bottom: 1px solid var(--gray-soft-dark); + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + + .notification-type { + flex-grow: 1; + } + + .form-actions { + display: flex; + gap: 20px; + flex-shrink: 0; + } + + .form-action { + width: 144px; + } + + @media (max-width: 1024px) { + flex-direction: column; + gap: 6px; + margin-bottom: 16px; + } +} diff --git a/app/frontend/src/stylesheets/modules/notifications/notifications-category.scss b/app/frontend/src/stylesheets/modules/notifications/notifications-category.scss new file mode 100644 index 000000000..f2c51b793 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/notifications/notifications-category.scss @@ -0,0 +1,52 @@ +.notifications-category { + max-width: 1200px; + display: flex; + justify-content: space-between; + gap: 32px; + + .category-name { + width: 288px; + flex-shrink: 0; + color: var(--gray-hard-darkest) !important; + @include title-base; + } + + .category-content { + max-width: 800px; + flex-grow: 1; + margin-top: 24px; + padding: 20px; + background-color: var(--gray-soft-light); + border: 1px solid var(--gray-soft-dark); + border-radius: 5px; + } + + .category-actions { + display: flex; + justify-content: end; + gap: 6px; + } + + .category-action { + background-color: #f5f5f5; + font-size: 15px; + text-transform: uppercase; + border: none; + margin-bottom: 3rem; + + &:hover { + background-color: var(--gray-soft-dark); + } + } + + @media (max-width: 1200px) { + flex-direction: column; + gap: 0px; + .category-name { + width: fit-content; + } + .category-content { + margin-bottom: 32px; + } + } +} diff --git a/app/models/notification_preference.rb b/app/models/notification_preference.rb index 530b1639f..55ac9d10b 100644 --- a/app/models/notification_preference.rb +++ b/app/models/notification_preference.rb @@ -4,4 +4,10 @@ class NotificationPreference < ApplicationRecord belongs_to :user belongs_to :notification_type + + validates :notification_type_id, uniqueness: { scope: :user } + + def notification_type + NotificationType.find(notification_type_id).name + end end diff --git a/app/policies/notification_preference_policy.rb b/app/policies/notification_preference_policy.rb new file mode 100644 index 000000000..9fa267868 --- /dev/null +++ b/app/policies/notification_preference_policy.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Check the access policies for API::NotificationController +class NotificationPreferencePolicy < ApplicationPolicy + def update? + user.admin? + end + + def bulk_update? + user.admin? + end +end diff --git a/app/views/api/notification_preferences/index.json.jbuilder b/app/views/api/notification_preferences/index.json.jbuilder new file mode 100644 index 000000000..c0a2ce8e1 --- /dev/null +++ b/app/views/api/notification_preferences/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array!(@notification_preferences) do |notification_preference| + json.extract! notification_preference, :id, :notification_type, :in_system, :email +end diff --git a/app/views/api/notification_preferences/show.json.jbuilder b/app/views/api/notification_preferences/show.json.jbuilder new file mode 100644 index 000000000..42081ae28 --- /dev/null +++ b/app/views/api/notification_preferences/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @notification_preference, :id, :notification_type, :in_system, :email diff --git a/app/views/api/notification_types/index.json.jbuilder b/app/views/api/notification_types/index.json.jbuilder new file mode 100644 index 000000000..c8844c658 --- /dev/null +++ b/app/views/api/notification_types/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array!(@notification_types) do |notification_type| + json.extract! notification_type, :id, :name, :category, :is_configurable +end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 86ad61656..ef17bfd54 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -2412,6 +2412,3 @@ en: cta_switch: "Display a button" cta_label: "Button label" cta_url: "Button link" - notifications_center: - notifications_list: "All notifications" - notifications_settings: "My notifications preferences" diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index 81ba609f7..1da3f0a90 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -260,5 +260,63 @@ en: archives: "Archives" no_archived_notifications: "No archived notifications." load_the_next_notifications: "Load the next notifications..." - notification_line: + notification_inline: mark_as_read: "Mark as read" + notifications_center: + notifications_list: "All notifications" + notifications_settings: "My notifications preferences" + notifications_category: + enable_all: "Enable all" + disable_all: "Disable all" + notify_me_when: "I wish to be notified when" + users_accounts: "Concerning users notifications" + proof_of_identity: "Concerning identity proofs notifications" + agenda: "Concerning agenda notifications" + subscriptions: "Concerning subscriptions notifications" + payments: "Concerning payment schedules notifications" + wallet: "Concerning wallet notifications" + shop: "Concerning shop notifications" + projects: "Concerning projects notifications" + accountings: "Concerning accounting notifications" + trainings: "Concerning trainings notifications" + app_management: "Concerning app management notifications" + notification_form: + notify_admin_when_user_is_created: "A user account has been created" + notify_admin_when_user_is_imported: "A user account has been imported" + notify_admin_profile_complete: "An imported account has completed its profile" + notify_admin_user_merged: "An imported account has been merged with an existing account" + notify_admins_role_update: "The role of a user has changed" + notify_admin_import_complete: "An import is done" + notify_admin_user_group_changed: "A group has changed" + notify_admin_user_proof_of_identity_refusal: "A proof of identity has been rejected" + notify_admin_user_proof_of_identity_files_created: "A user has uploaded a proof of identity" + notify_admin_user_proof_of_identity_files_updated: "A user has updated a proof of identity" + notify_admin_member_create_reservation: "A member creates a reservation" + notify_admin_slot_is_modified: "A reservation slot has been modified" + notify_admin_slot_is_canceled: "A reservation has been cancelled" + notify_admin_subscribed_plan: "A subscription has been purchased" + notify_admin_subscription_will_expire_in_7_days: "A member subscription expires in 7 days" + notify_admin_subscription_is_expired: "A member subscription has expired" + notify_admin_subscription_extended: "A subscription has been extended" + notify_admin_subscription_canceled: "A member subscription has been cancelled" + notify_admin_payment_schedule_failed: "Card debit failure" + notify_admin_payment_schedule_check_deadline: "A payment deadline is soon" + notify_admin_payment_schedule_transfer_deadline: "A bank direct debit has to be confirmed" + notify_admin_payment_schedule_error: "An error occurred for the card debit for a schedule" + notify_admin_gateway_canceled: "You must confirm a bank direct debit for for a schedule" + notify_admin_refund_created: "A refund has been created" + notify_admin_user_wallet_is_credited: "The wallet of an user has been credited" + notify_user_order_is_ready: "Your command is ready" + notify_user_order_is_canceled: "Your command was canceled" + notify_user_order_is_refunded: "Your command was refunded" + notify_admin_low_stock_threshold: "The stock is low" + notify_admin_when_project_published: "A project has been published" + notify_admin_abuse_reported: "An abusive content has been reported" + notify_admin_close_period_reminder: "An accounting period has to be closed soon" + notify_admin_archive_complete: "An archive is completed" + notify_admin_training_auto_cancelled: "A training was automatically cancelled" + notify_admin_export_complete: "An export is completed" + notify_user_when_invoice_ready: "An invoice is available" + notify_admin_payment_schedule_gateway_canceled: "A payment schedule has been canceled by the payment gateway" + notify_project_collaborator_to_valid: "You are invited to collaborate on the project" + notify_project_author_when_collaborator_valid: "A collaborator has accepted your invitation to join your project" diff --git a/config/routes.rb b/config/routes.rb index 79b2566e3..41a066972 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,6 +72,10 @@ Rails.application.routes.draw do get 'polling', action: 'polling', on: :collection get 'last_unread', action: 'last_unread', on: :collection end + resources :notification_types, only: %i[index] + resources :notification_preferences, only: %i[index update], param: :notification_type do + patch '/bulk_update', action: 'bulk_update', on: :collection + end resources :wallet, only: [] do get '/by_user/:user_id', action: 'by_user', on: :collection get :transactions, on: :member diff --git a/db/migrate/20230127100506_create_notification_preferences.rb b/db/migrate/20230127100506_create_notification_preferences.rb index f80d66f15..56ea4702f 100644 --- a/db/migrate/20230127100506_create_notification_preferences.rb +++ b/db/migrate/20230127100506_create_notification_preferences.rb @@ -5,11 +5,13 @@ class CreateNotificationPreferences < ActiveRecord::Migration[5.2] def change create_table :notification_preferences do |t| - t.references :user, index: true, foreign_key: true, null: false + t.references :user, index: false, foreign_key: true, null: false t.references :notification_type, index: true, foreign_key: true, null: false t.boolean :in_system, default: true t.boolean :email, default: true + t.index %i[user_id notification_type_id], unique: true, name: :index_notification_preferences_on_user_and_notification_type + t.timestamps end end