diff --git a/app/controllers/api/v2/alerts_controller.rb b/app/controllers/api/v2/alerts_controller.rb
index 11b25b43a1..efc0227f0a 100644
--- a/app/controllers/api/v2/alerts_controller.rb
+++ b/app/controllers/api/v2/alerts_controller.rb
@@ -17,6 +17,17 @@ def index
@alerts = @record.alerts
end
+ def destroy
+ authorize! :remove_alert, @record
+ alert_id = params[:id]
+ alert = @record.alerts.find { |a| a.unique_id == alert_id }
+ if alert.present?
+ alert.destroy!
+ return
+ end
+ raise ActiveRecord::RecordNotFound
+ end
+
def index_action_message
'show_alerts'
end
diff --git a/app/javascript/components/internal-alert/component.jsx b/app/javascript/components/internal-alert/component.jsx
index 8af0fd0e81..88c7d193a0 100644
--- a/app/javascript/components/internal-alert/component.jsx
+++ b/app/javascript/components/internal-alert/component.jsx
@@ -15,6 +15,7 @@ import SignalWifiOffIcon from "@material-ui/icons/SignalWifiOff";
import { generate } from "../notifier/utils";
import { useI18n } from "../i18n";
+import InternalAlertItem from "./components/item";
import { NAME, SEVERITY } from "./constants";
import { expansionPanelSummaryClasses } from "./theme";
import css from "./styles.css";
@@ -38,7 +39,9 @@ const Component = ({ title, items, severity, customIcon }) => {
{items.map(item => (
- - {item.get("message")}
+ -
+
+
))}
@@ -61,12 +64,16 @@ const Component = ({ title, items, severity, customIcon }) => {
const renderTitle = () => {
const titleMessage =
- items?.size > 1 ? title || i18n.t("messages.alert_items", { items: items.size }) : items?.first()?.get("message");
+ items?.size > 1 ? (
+ title ||
{i18n.t("messages.alert_items", { items: items.size })}
+ ) : (
+
+ );
return (
<>
{customIcon || renderIcon()}
- {titleMessage}
+ {titleMessage}
>
);
};
diff --git a/app/javascript/components/internal-alert/component.unit.test.js b/app/javascript/components/internal-alert/component.unit.test.js
index 55c204d89f..be347092d9 100644
--- a/app/javascript/components/internal-alert/component.unit.test.js
+++ b/app/javascript/components/internal-alert/component.unit.test.js
@@ -1,7 +1,7 @@
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
import { fromJS } from "immutable";
-import { AccordionDetails, AccordionSummary } from "@material-ui/core";
+import { AccordionDetails, AccordionSummary, IconButton } from "@material-ui/core";
import { setupMountedComponent } from "../../test";
@@ -62,4 +62,30 @@ describe("", () => {
expect(component.find(AccordionSummary).text()).to.be.equal(title);
});
+
+ it("renders a dismiss button if onDismiss is on an item", () => {
+ const { component } = setupMountedComponent(
+ InternalAlert,
+ {
+ items: fromJS([{ message: "Alert Message 1", onDismiss: () => {} }]),
+ severity: "warning"
+ },
+ {}
+ );
+
+ expect(component.find(IconButton)).to.have.lengthOf(1);
+ });
+
+ it("does not render a dismiss button if onDismiss is not on an item", () => {
+ const { component } = setupMountedComponent(
+ InternalAlert,
+ {
+ items: fromJS([{ message: "Alert Message 1" }]),
+ severity: "warning"
+ },
+ {}
+ );
+
+ expect(component.find(IconButton)).to.have.lengthOf(0);
+ });
});
diff --git a/app/javascript/components/internal-alert/components/dismiss-button/component.jsx b/app/javascript/components/internal-alert/components/dismiss-button/component.jsx
new file mode 100644
index 0000000000..ff40077461
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/dismiss-button/component.jsx
@@ -0,0 +1,24 @@
+import { IconButton } from "@material-ui/core";
+import CloseIcon from "@material-ui/icons/Close";
+import PropTypes from "prop-types";
+
+import css from "./styles.css";
+
+const Component = ({ handler }) => {
+ const handlerWrapper = event => {
+ event.stopPropagation();
+ handler();
+ };
+
+ return (
+
+
+
+ );
+};
+
+Component.displayName = "InternalAlertDismissButton";
+Component.propTypes = {
+ handler: PropTypes.func.isRequired
+};
+export default Component;
diff --git a/app/javascript/components/internal-alert/components/dismiss-button/index.js b/app/javascript/components/internal-alert/components/dismiss-button/index.js
new file mode 100644
index 0000000000..b6e0586481
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/dismiss-button/index.js
@@ -0,0 +1 @@
+export { default } from "./component";
diff --git a/app/javascript/components/internal-alert/components/dismiss-button/styles.css b/app/javascript/components/internal-alert/components/dismiss-button/styles.css
new file mode 100644
index 0000000000..6626149cff
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/dismiss-button/styles.css
@@ -0,0 +1,3 @@
+.dismissButton {
+ pointer-events: auto;
+}
diff --git a/app/javascript/components/internal-alert/components/item/component.jsx b/app/javascript/components/internal-alert/components/item/component.jsx
new file mode 100644
index 0000000000..ed347a2715
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/item/component.jsx
@@ -0,0 +1,21 @@
+import PropTypes from "prop-types";
+
+import InternalAlertDismissButton from "../dismiss-button";
+
+import css from "./styles.css";
+
+const Component = ({ item }) => {
+ return (
+
+ {item.get("message")}
+ {item.get("onDismiss") && InternalAlertDismissButton({ handler: item.get("onDismiss") })}
+
+ );
+};
+
+Component.displayName = "InternalAlertItem";
+Component.propTypes = {
+ item: PropTypes.object.isRequired
+};
+
+export default Component;
diff --git a/app/javascript/components/internal-alert/components/item/index.js b/app/javascript/components/internal-alert/components/item/index.js
new file mode 100644
index 0000000000..b6e0586481
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/item/index.js
@@ -0,0 +1 @@
+export { default } from "./component";
diff --git a/app/javascript/components/internal-alert/components/item/styles.css b/app/javascript/components/internal-alert/components/item/styles.css
new file mode 100644
index 0000000000..9c0774433a
--- /dev/null
+++ b/app/javascript/components/internal-alert/components/item/styles.css
@@ -0,0 +1,7 @@
+.alertItemElement {
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: space-between;
+ flex-grow: 1;
+}
diff --git a/app/javascript/components/internal-alert/styles.css b/app/javascript/components/internal-alert/styles.css
index 6907d7949c..155e775854 100644
--- a/app/javascript/components/internal-alert/styles.css
+++ b/app/javascript/components/internal-alert/styles.css
@@ -4,10 +4,11 @@
& svg {
font-size: var(--fs-16);
}
+ align-self: center;
}
.alertTitle {
- display: inline-flex;
+ display: flex;
align-items: center;
width: 100%;
@@ -73,3 +74,7 @@
}
}
}
+
+.accordionTitle {
+ align-self: center;
+}
diff --git a/app/javascript/components/permissions/constants.js b/app/javascript/components/permissions/constants.js
index f51331d43e..4618be28cb 100644
--- a/app/javascript/components/permissions/constants.js
+++ b/app/javascript/components/permissions/constants.js
@@ -116,7 +116,8 @@ export const ACTIONS = {
WORKFLOW_REPORT: "workflow_report",
WRITE: "write",
VIEW_FAMILY_RECORD: "view_family_record",
- LINK_FAMILY_RECORD: "link_family_record"
+ LINK_FAMILY_RECORD: "link_family_record",
+ REMOVE_ALERT: "remove_alert"
};
export const MANAGE = [ACTIONS.MANAGE];
@@ -343,3 +344,5 @@ export const CREATE_CASE_FROM_FAMILY = [...MANAGE, ACTIONS.CASE_FROM_FAMILY];
export const LINK_FAMILY_RECORD_FROM_CASE = [...MANAGE, ACTIONS.LINK_FAMILY_RECORD];
export const VIEW_FAMILY_RECORD_FROM_CASE = [...MANAGE, ACTIONS.VIEW_FAMILY_RECORD];
+
+export const REMOVE_ALERT = [...MANAGE, ACTIONS.REMOVE_ALERT];
diff --git a/app/javascript/components/permissions/index.js b/app/javascript/components/permissions/index.js
index 63264a058c..c309ea1fe4 100644
--- a/app/javascript/components/permissions/index.js
+++ b/app/javascript/components/permissions/index.js
@@ -50,7 +50,8 @@ export {
VIEW_INCIDENTS_FROM_CASE,
VIEW_KPIS,
WRITE_RECORDS,
- WRITE_REGISTRY_RECORD
+ WRITE_REGISTRY_RECORD,
+ REMOVE_ALERT
} from "./constants";
export { checkPermissions } from "./utils";
diff --git a/app/javascript/components/record-form-alerts/component.jsx b/app/javascript/components/record-form-alerts/component.jsx
index 9898d8d079..d10c6cebd2 100644
--- a/app/javascript/components/record-form-alerts/component.jsx
+++ b/app/javascript/components/record-form-alerts/component.jsx
@@ -2,24 +2,34 @@
import PropTypes from "prop-types";
import { fromJS, List } from "immutable";
+import { useDispatch } from "react-redux";
import { useI18n } from "../i18n";
import InternalAlert from "../internal-alert";
import useMemoizedSelector from "../../libs/use-memoized-selector";
-import { getRecordFormAlerts } from "../records";
+import { getRecordFormAlerts, getSelectedRecord, deleteAlertFromRecord } from "../records";
import { getSubformsDisplayName, getValidationErrors } from "../record-form";
import { getDuplicatedFields } from "../record-form/selectors";
+import { usePermissions, REMOVE_ALERT } from "../permissions";
import { getMessageData } from "./utils";
import { NAME } from "./constants";
-const Component = ({ form, recordType, attachmentForms }) => {
+const Component = ({ form, recordType, attachmentForms, formMode }) => {
const i18n = useI18n();
+ const dispatch = useDispatch();
+
const recordAlerts = useMemoizedSelector(state => getRecordFormAlerts(state, recordType, form.unique_id));
const validationErrors = useMemoizedSelector(state => getValidationErrors(state, form.unique_id));
const subformDisplayNames = useMemoizedSelector(state => getSubformsDisplayName(state, i18n.locale));
const duplicatedFields = useMemoizedSelector(state => getDuplicatedFields(state, recordType, form.unique_id));
+ const selectedRecord = useMemoizedSelector(state => getSelectedRecord(state, recordType));
+ const hasDismissPermission = usePermissions(recordType, REMOVE_ALERT);
+
+ const showDismissButton = () => {
+ return hasDismissPermission && formMode.isShow;
+ };
const errors =
validationErrors?.size &&
@@ -44,7 +54,12 @@ const Component = ({ form, recordType, attachmentForms }) => {
message: i18n.t(
`messages.alerts_for.${alert.get("alert_for")}`,
getMessageData({ alert, form, duplicatedFields, i18n })
- )
+ ),
+ onDismiss: showDismissButton()
+ ? () => {
+ dispatch(deleteAlertFromRecord(recordType, selectedRecord, alert.get("unique_id")));
+ }
+ : null
})
);
@@ -73,6 +88,7 @@ Component.defaultProps = {
Component.propTypes = {
attachmentForms: PropTypes.object,
form: PropTypes.object.isRequired,
+ formMode: PropTypes.object,
recordType: PropTypes.string.isRequired
};
diff --git a/app/javascript/components/record-form/components/render-form-sections.js b/app/javascript/components/record-form/components/render-form-sections.js
index 7e6a6a2bb5..99ec127f1f 100644
--- a/app/javascript/components/record-form/components/render-form-sections.js
+++ b/app/javascript/components/record-form/components/render-form-sections.js
@@ -114,7 +114,7 @@ const renderFormSections =
{...titleProps}
/>
-
+
{renderFormFields(
fs,
form,
diff --git a/app/javascript/components/records/action-creators.js b/app/javascript/components/records/action-creators.js
index 79f386e453..350d3dc341 100644
--- a/app/javascript/components/records/action-creators.js
+++ b/app/javascript/components/records/action-creators.js
@@ -42,7 +42,9 @@ import {
EXTERNAL_SYNC,
OFFLINE_INCIDENT_FROM_CASE,
CREATE_CASE_FROM_FAMILY_MEMBER,
- CREATE_CASE_FROM_FAMILY_DETAIL
+ CREATE_CASE_FROM_FAMILY_DETAIL,
+ DELETE_ALERT_FROM_RECORD,
+ DELETE_ALERT_FROM_RECORD_SUCCESS
} from "./actions";
const getSuccessCallback = ({
@@ -170,6 +172,20 @@ export const fetchRecordsAlerts = (recordType, recordId, asCallback = false) =>
}
});
+export const deleteAlertFromRecord = (recordType, recordId, alertId) => ({
+ type: `${recordType}/${DELETE_ALERT_FROM_RECORD}`,
+ api: {
+ path: `${recordType}/${recordId}/alerts/${alertId}`,
+ method: METHODS.DELETE,
+ skipDB: true,
+ performFromQueue: true,
+ successCallback: {
+ action: `${recordType}/${DELETE_ALERT_FROM_RECORD_SUCCESS}`,
+ payload: { alertId }
+ }
+ }
+});
+
export const saveRecord = (
recordType,
saveMethod,
diff --git a/app/javascript/components/records/action-creators.unit.test.js b/app/javascript/components/records/action-creators.unit.test.js
index 0dd64f989c..ea5e6249fb 100644
--- a/app/javascript/components/records/action-creators.unit.test.js
+++ b/app/javascript/components/records/action-creators.unit.test.js
@@ -53,7 +53,8 @@ describe("records - Action Creators", () => {
"setSelectedCasePotentialMatch",
"setSelectedPotentialMatch",
"setSelectedRecord",
- "unMatchCaseForTrace"
+ "unMatchCaseForTrace",
+ "deleteAlertFromRecord"
].forEach(property => {
expect(creators).to.have.property(property);
expect(creators[property]).to.be.a("function");
@@ -537,4 +538,22 @@ describe("records - Action Creators", () => {
expect(actionCreators.clearPotentialMatches()).be.deep.equals(expected);
});
+
+ it("checks that 'deleteAlertFromRecord' action creator to return the correct object", () => {
+ const expected = {
+ type: `${RECORD_PATH.cases}/DELETE_ALERT_FROM_RECORD`,
+ api: {
+ path: `${RECORD_PATH.cases}/12345/alerts/12345-alert`,
+ method: METHODS.DELETE,
+ skipDB: true,
+ performFromQueue: true,
+ successCallback: {
+ action: `${RECORD_PATH.cases}/DELETE_ALERT_FROM_RECORD_SUCCESS`,
+ payload: { alertId: "12345-alert" }
+ }
+ }
+ };
+
+ expect(actionCreators.deleteAlertFromRecord(RECORD_PATH.cases, "12345", "12345-alert")).be.deep.equals(expected);
+ });
});
diff --git a/app/javascript/components/records/actions.js b/app/javascript/components/records/actions.js
index 989aa3c185..1403280a36 100644
--- a/app/javascript/components/records/actions.js
+++ b/app/javascript/components/records/actions.js
@@ -30,6 +30,11 @@ export const FETCH_RECORD_ALERTS_FAILURE = "FETCH_RECORD_ALERTS_FAILURE";
export const FETCH_RECORD_ALERTS_FINISHED = "FETCH_RECORD_ALERTS_FINISHED";
export const FETCH_RECORD_ALERTS_STARTED = "FETCH_RECORD_ALERTS_STARTED";
export const FETCH_RECORD_ALERTS_SUCCESS = "FETCH_RECORD_ALERTS_SUCCESS";
+export const DELETE_ALERT_FROM_RECORD = "DELETE_ALERT_FROM_RECORD";
+export const DELETE_ALERT_FROM_RECORD_FAILURE = "DELETE_ALERT_FROM_RECORD_FAILURE";
+export const DELETE_ALERT_FROM_RECORD_FINISHED = "DELETE_ALERT_FROM_RECORD_FINISHED";
+export const DELETE_ALERT_FROM_RECORD_STARTED = "DELETE_ALERT_FROM_RECORD_STARTED";
+export const DELETE_ALERT_FROM_RECORD_SUCCESS = "DELETE_ALERT_FROM_RECORD_SUCCESS";
export const FETCH_INCIDENT_FROM_CASE = "FETCH_INCIDENT_FROM_CASE";
export const FETCH_INCIDENT_FROM_CASE_FAILURE = "FETCH_INCIDENT_FROM_CASE_FAILURE";
export const FETCH_INCIDENT_FROM_CASE_FINISHED = "FETCH_INCIDENT_FROM_CASE_FINISHED";
diff --git a/app/javascript/components/records/actions.unit.test.js b/app/javascript/components/records/actions.unit.test.js
index b5ca26ee03..26aa5cb005 100644
--- a/app/javascript/components/records/actions.unit.test.js
+++ b/app/javascript/components/records/actions.unit.test.js
@@ -100,7 +100,12 @@ describe("records - Actions", () => {
"CREATE_CASE_FROM_FAMILY_DETAIL_SUCCESS",
"CREATE_CASE_FROM_FAMILY_DETAIL_STARTED",
"CREATE_CASE_FROM_FAMILY_DETAIL_FAILURE",
- "CREATE_CASE_FROM_FAMILY_DETAIL_FINISHED"
+ "CREATE_CASE_FROM_FAMILY_DETAIL_FINISHED",
+ "DELETE_ALERT_FROM_RECORD",
+ "DELETE_ALERT_FROM_RECORD_FAILURE",
+ "DELETE_ALERT_FROM_RECORD_FINISHED",
+ "DELETE_ALERT_FROM_RECORD_STARTED",
+ "DELETE_ALERT_FROM_RECORD_SUCCESS"
].forEach(property => {
expect(cloneActions).to.have.property(property);
expect(cloneActions[property]).to.be.a("string");
diff --git a/app/javascript/components/records/reducer.js b/app/javascript/components/records/reducer.js
index 54d4f21542..d6c03eccb5 100644
--- a/app/javascript/components/records/reducer.js
+++ b/app/javascript/components/records/reducer.js
@@ -21,6 +21,7 @@ import {
RECORD_FINISHED,
SERVICE_REFERRED_SAVE,
FETCH_RECORD_ALERTS_SUCCESS,
+ DELETE_ALERT_FROM_RECORD_SUCCESS,
FETCH_INCIDENT_FROM_CASE_SUCCESS,
CLEAR_METADATA,
CLEAR_CASE_FROM_INCIDENT,
@@ -178,6 +179,13 @@ export default namespace =>
return state.set("errors", true);
case `${namespace}/${RECORD_FINISHED}`:
return state.set("loading", false);
+ case `${namespace}/${DELETE_ALERT_FROM_RECORD_SUCCESS}`:
+ state.set("alert_count", state.get("alert_count") - 1);
+
+ return state.set(
+ "recordAlerts",
+ state.get("recordAlerts").filter(alert => alert.get("unique_id") !== payload.alertId)
+ );
case `${namespace}/${FETCH_RECORD_ALERTS_SUCCESS}`:
return state.set("recordAlerts", fromJS(payload.data));
case "user/LOGOUT_SUCCESS":
diff --git a/app/javascript/components/records/reducer.unit.test.js b/app/javascript/components/records/reducer.unit.test.js
index 5cfb0dfd0e..14d2c8cd03 100644
--- a/app/javascript/components/records/reducer.unit.test.js
+++ b/app/javascript/components/records/reducer.unit.test.js
@@ -812,4 +812,32 @@ describe(" - Reducers", () => {
expect(newState).to.deep.equals(expected);
});
+
+ it("should handle DELETE_ALERT_FROM_RECORD_SUCCESS", () => {
+ const initialState = fromJS({
+ alert_count: 1,
+ recordAlerts: [
+ {
+ alert_for: "field_change",
+ type: "formsection-stuff-with-subform-field-d6055c8",
+ date: "2023-10-16",
+ form_unique_id: "formsection-stuff-with-subform-field-d6055c8",
+ unique_id: "018a28dd-4af4-4521-91ed-636efed6228e"
+ }
+ ]
+ });
+ const expected = fromJS({
+ alert_count: 0,
+ recordAlerts: []
+ });
+ const action = {
+ type: "TestRecordType/DELETE_ALERT_FROM_RECORD_SUCCESS",
+ payload: {
+ alertId: "018a28dd-4af4-4521-91ed-636efed6228e"
+ }
+ };
+ const newState = nsReducer(initialState, action);
+
+ expect(newState).to.deep.equals(newState);
+ });
});
diff --git a/app/jobs/alert_notify_job.rb b/app/jobs/alert_notify_job.rb
new file mode 100644
index 0000000000..00950fa89f
--- /dev/null
+++ b/app/jobs/alert_notify_job.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# Job for enqueuing notifications about changes to the User account
+class AlertNotifyJob < ApplicationJob
+ queue_as :mailer
+
+ def perform(alert_id, user_id)
+ alert = Alert.find(alert_id)
+ user = User.find(user_id)
+ record = alert.record
+ # We never want to send an email to the user who made the change
+ return if record.last_updated_by == user.user_name
+
+ ans = AlertNotificationService.new(record.id, alert_id, user.user_name)
+ RecordActionMailer.alert_notify(ans).deliver_now
+ RecordActionWebpushNotifier.alert_notify(ans)
+ end
+end
diff --git a/app/mailers/record_action_mailer.rb b/app/mailers/record_action_mailer.rb
index 385c687b3b..e5675dbc36 100644
--- a/app/mailers/record_action_mailer.rb
+++ b/app/mailers/record_action_mailer.rb
@@ -44,6 +44,16 @@ def transfer_request(transfer_request_notification)
mail(to: @transfer_request_notification&.transitioned_to&.email, subject: @transfer_request_notification.subject)
end
+ def alert_notify(alert_notification)
+ @alert_notification = alert_notification
+ return unless assert_notifications_enabled(@alert_notification.user)
+ return if @alert_notification.user == @alert_notification.record.last_updated_by
+
+ Rails.logger.info("Sending alert notification to #{@alert_notification.user.user_name}")
+
+ mail(to: @alert_notification.user.email, subject: @alert_notification.subject, locale: @alert_notification.locale)
+ end
+
private
def assert_notifications_enabled(user)
diff --git a/app/models/alert.rb b/app/models/alert.rb
index c2375906bf..add359c69c 100644
--- a/app/models/alert.rb
+++ b/app/models/alert.rb
@@ -8,9 +8,11 @@ class Alert < ApplicationRecord
belongs_to :agency, optional: true
belongs_to :user, optional: true
validates :alert_for, presence: { message: 'errors.models.alerts.alert_for' }
+ attribute :send_email, :boolean, default: false
before_create :generate_fields
before_create :remove_duplicate_alert
+ after_create_commit :handle_send_email
def generate_fields
self.unique_id ||= SecureRandom.uuid
@@ -26,4 +28,13 @@ def remove_duplicate_alert
DuplicatedFieldAlertService.duplicate_alert(record, type)&.destroy!
end
+
+ def handle_send_email
+ return unless send_email
+
+ users = record.associated_users
+ users.each do |user|
+ AlertNotifyJob.perform_later(id, user.id)
+ end
+ end
end
diff --git a/app/models/concerns/alertable.rb b/app/models/concerns/alertable.rb
index 1425b8bac8..f4daafb2aa 100644
--- a/app/models/concerns/alertable.rb
+++ b/app/models/concerns/alertable.rb
@@ -14,6 +14,15 @@ module Alertable
TRANSFER_REQUEST = 'transfer_request'
INCIDENT_FROM_CASE = 'incident_from_case'
+ module AlertStrategy
+ # This sends email (and webpush) notifications to all
+ # users on the case other than the one making the change
+ ASSOCIATED_USERS = 'associated_users'
+ # This doesn't send email notifications to anyone, and only creates a yellow
+ # dot alert if the user making the change is not the owner
+ NOT_OWNER = 'not_owner' # this is the default
+ end
+
included do
searchable do
string :current_alert_types, multiple: true
@@ -42,18 +51,38 @@ def remove_alert_on_save
end
def remove_field_change_alerts
- alerts_on_change.each { |_, form_name| remove_alert(form_name) }
+ alerts_on_change.each do |_, conf_record|
+ next if conf_record.alert_strategy == AlertStrategy::ASSOCIATED_USERS
+
+ remove_alert(conf_record.form_section_unique_id)
+ end
end
def add_alert_on_field_change
- return unless owned_by != last_updated_by
return unless alerts_on_change.present?
changed_field_names = changes_to_save_for_record.keys
- alerts_on_change.each do |field_name, form_name|
+ alerts_on_change.each do |field_name, conf_record|
next unless changed_field_names.include?(field_name)
- add_alert(alert_for: FIELD_CHANGE, date: Date.today, type: form_name, form_sidebar_id: form_name)
+ # remove any existing alerts of the same type
+ remove_alert(conf_record.form_section_unique_id)
+ add_field_alert(conf_record)
+ end
+ end
+
+ def add_field_alert(conf_record)
+ case conf_record.alert_strategy
+ when AlertStrategy::ASSOCIATED_USERS
+ add_alert(alert_for: FIELD_CHANGE, date: Date.today, type: conf_record.form_section_unique_id,
+ form_sidebar_id: conf_record.form_section_unique_id, send_email: true)
+
+ when AlertStrategy::NOT_OWNER
+ return if owned_by == last_updated_by
+
+ add_alert(alert_for: FIELD_CHANGE, date: Date.today, type: conf_record.form_section_unique_id,
+ form_sidebar_id: conf_record.form_section_unique_id)
+ else raise "Unknown alert strategy #{conf_record.alert_strategy}"
end
end
@@ -65,7 +94,8 @@ def add_alert(args = {})
date_alert = args[:date].presence || Date.today
alert = Alert.new(type: args[:type], date: date_alert, form_sidebar_id: args[:form_sidebar_id],
- alert_for: args[:alert_for], user_id: args[:user_id], agency_id: args[:agency_id])
+ alert_for: args[:alert_for], user_id: args[:user_id], agency_id: args[:agency_id],
+ send_email: args[:send_email])
(alerts << alert) && alert
end
@@ -93,53 +123,60 @@ def add_approval_alert(approval_type, system_settings)
def alerts_on_change
@system_settings ||= SystemSettings.current
- @system_settings&.changes_field_to_form
- end
-
- # Class methods that indicate alerts for all permitted records for a user.
- # TODO: This deserves its own service
- module ClassMethods
- def alert_count(current_user)
- query_scope = current_user.record_query_scope(self.class)[:user]
- if query_scope.blank?
- open_enabled_records.distinct.count
- elsif query_scope[Permission::AGENCY].present?
- alert_count_agency(current_user)
- elsif query_scope[Permission::GROUP].present?
- alert_count_group(current_user)
- else
- alert_count_self(current_user)
- end
- end
-
- def remove_alert(type = nil)
- alerts_to_delete = alerts.select do |alert|
- type.present? && alert.type == type && [NEW_FORM, FIELD_CHANGE, TRANSFER_REQUEST].include?(alert.alert_for)
- end
+ # changes field to form needs to be backwards compatible, so each of the
+ # values in the hash is either a string or a hash. If it's a string, it's
+ # the form section unique id. If it's a hash, it's the form section unique
+ # id and the alert strategy
+ (
+ @system_settings&.changes_field_to_form&.map do |field_name, form_section_uid_or_hash|
+ [field_name, AlertConfigEntryService.new(form_section_uid_or_hash)]
+ end).to_h
+ end
+end
- alerts.destroy(*alerts_to_delete)
+# Class methods that indicate alerts for all permitted records for a user.
+# TODO: This deserves its own service
+module ClassMethods
+ def alert_count(current_user)
+ query_scope = current_user.record_query_scope(self.class)[:user]
+ if query_scope.blank?
+ open_enabled_records.distinct.count
+ elsif query_scope[Permission::AGENCY].present?
+ alert_count_agency(current_user)
+ elsif query_scope[Permission::GROUP].present?
+ alert_count_group(current_user)
+ else
+ alert_count_self(current_user)
end
+ end
- def alert_count_agency(current_user)
- agency_unique_id = current_user.agency.unique_id
- open_enabled_records.where("data -> 'associated_user_agencies' ? :agency", agency: agency_unique_id)
- .distinct.count
+ def remove_alert(type = nil)
+ alerts_to_delete = alerts.select do |alert|
+ type.present? && alert.type == type && [NEW_FORM, FIELD_CHANGE, TRANSFER_REQUEST].include?(alert.alert_for)
end
- def alert_count_group(current_user)
- user_groups_unique_id = current_user.user_groups.pluck(:unique_id)
- open_enabled_records.where(
- "data -> 'associated_user_groups' ?& array[:group]",
- group: user_groups_unique_id
- ).distinct.count
- end
+ alerts.destroy(*alerts_to_delete)
+ end
- def alert_count_self(current_user)
- open_enabled_records.owned_by(current_user.user_name).distinct.count
- end
+ def alert_count_agency(current_user)
+ agency_unique_id = current_user.agency.unique_id
+ open_enabled_records.where("data -> 'associated_user_agencies' ? :agency", agency: agency_unique_id)
+ .distinct.count
+ end
- def open_enabled_records
- joins(:alerts).where('data @> ?', { record_state: true, status: Record::STATUS_OPEN }.to_json)
- end
+ def alert_count_group(current_user)
+ user_groups_unique_id = current_user.user_groups.pluck(:unique_id)
+ open_enabled_records.where(
+ "data -> 'associated_user_groups' ?& array[:group]",
+ group: user_groups_unique_id
+ ).distinct.count
+ end
+
+ def alert_count_self(current_user)
+ open_enabled_records.owned_by(current_user.user_name).distinct.count
+ end
+
+ def open_enabled_records
+ joins(:alerts).where('data @> ?', { record_state: true, status: Record::STATUS_OPEN }.to_json)
end
end
diff --git a/app/models/exporters/role_permissions_exporter.rb b/app/models/exporters/role_permissions_exporter.rb
index fb06fa2d60..cb7eac9fac 100644
--- a/app/models/exporters/role_permissions_exporter.rb
+++ b/app/models/exporters/role_permissions_exporter.rb
@@ -12,7 +12,7 @@ class Exporters::RolePermissionsExporter
CASE = %w[
referral transfer read create write enable_disable_record flag manage add_note reopen close
change_log view_incident_from_case view_protection_concerns_filter list_case_names view_registry_record
- add_registry_record view_family_record case_from_family link_family_record
+ add_registry_record view_family_record case_from_family link_family_record remove_alert
].freeze
CASE_EXPORTS = %w[
export_list_view_csv export_csv export_xls export_photowall export_unhcr_csv export_pdf consent_override
diff --git a/app/models/permission.rb b/app/models/permission.rb
index f7e639bfcd..ddc2925c62 100644
--- a/app/models/permission.rb
+++ b/app/models/permission.rb
@@ -174,6 +174,7 @@ class Permission < ValueObject
CASE_FROM_FAMILY = 'case_from_family'
REFERRALS_TRANSFERS_REPORT = 'referrals_transfers_report'
LINK_FAMILY_RECORD = 'link_family_record'
+ REMOVE_ALERT = 'remove_alert'
RESOURCE_ACTIONS = {
CASE => [
@@ -188,12 +189,12 @@ class Permission < ValueObject
FIND_TRACING_MATCH, ASSIGN, ASSIGN_WITHIN_AGENCY, ASSIGN_WITHIN_USER_GROUP, REMOVE_ASSIGNED_USERS, TRANSFER,
RECEIVE_TRANSFER, ACCEPT_OR_REJECT_TRANSFER, REFERRAL, RECEIVE_REFERRAL, RECEIVE_REFERRAL_DIFFERENT_MODULE,
REOPEN, CLOSE, VIEW_PROTECTION_CONCERNS_FILTER, CHANGE_LOG, LIST_CASE_NAMES, VIEW_REGISTRY_RECORD,
- ADD_REGISTRY_RECORD, VIEW_FAMILY_RECORD, CASE_FROM_FAMILY, LINK_FAMILY_RECORD, MANAGE
+ ADD_REGISTRY_RECORD, VIEW_FAMILY_RECORD, CASE_FROM_FAMILY, LINK_FAMILY_RECORD, REMOVE_ALERT, MANAGE
],
INCIDENT => [
READ, CREATE, WRITE, ENABLE_DISABLE_RECORD, FLAG, EXPORT_LIST_VIEW, EXPORT_CSV, EXPORT_EXCEL, EXPORT_PDF,
EXPORT_INCIDENT_RECORDER, EXPORT_JSON, EXPORT_CUSTOM, IMPORT, SYNC_MOBILE, CHANGE_LOG, EXPORT_MRM_VIOLATION_XLS,
- MANAGE
+ REMOVE_ALERT, MANAGE
],
TRACING_REQUEST => [
READ, CREATE, WRITE, ENABLE_DISABLE_RECORD, FLAG, EXPORT_LIST_VIEW, EXPORT_CSV, EXPORT_EXCEL, EXPORT_PDF,
diff --git a/app/services/alert_config_entry_service.rb b/app/services/alert_config_entry_service.rb
new file mode 100644
index 0000000000..ec0274ad62
--- /dev/null
+++ b/app/services/alert_config_entry_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# This class is used for the members of changes_field_to_form in system_settings.
+# It is used to store the form name, and the alert strategy (associated_users, owner, nobody)
+class AlertConfigEntryService
+ attr_accessor :form_section_unique_id, :alert_strategy
+
+ def initialize(args)
+ if args.is_a?(Hash)
+ @form_section_unique_id = args['form_section_unique_id']
+ @alert_strategy = args['alert_strategy'] || Alertable::AlertStrategy::NOT_OWNER
+
+ else
+ @form_section_unique_id = args
+ @alert_strategy = Alertable::AlertStrategy::NOT_OWNER
+ end
+ end
+end
diff --git a/app/services/alert_notification_service.rb b/app/services/alert_notification_service.rb
new file mode 100644
index 0000000000..52c3545fb3
--- /dev/null
+++ b/app/services/alert_notification_service.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+# This service is for sending notifications when an alert with send_email: true is created.
+class AlertNotificationService
+ attr_accessor :record_id, :alert_id, :user_name
+
+ def initialize(record_id, alert_id, user_name)
+ self.record_id = record_id
+ self.alert_id = alert_id
+ self.user_name = user_name
+ end
+
+ def key
+ # This should return alert_case or alert_incident or similar based on the record type.
+ "alert_#{record.class.parent_form}"
+ end
+
+ def user
+ @user ||= User.find_by(user_name:)
+ log_not_found('User', user_name) if @user.blank?
+ @user
+ end
+
+ def alert
+ @alert ||= Alert.find_by(id: alert_id)
+ log_not_found('Alert', alert_id) if @alert.blank?
+ @alert
+ end
+
+ def record
+ @record ||= alert&.record
+ log_not_found('Record', alert&.record_id) if @record.blank?
+ @record
+ end
+
+ def locale
+ @locale ||= user&.locale || I18n.locale
+ end
+
+ def form_section
+ @form_section ||= FormSection.find_by(unique_id: alert.form_sidebar_id)
+ end
+
+ def record_type_translated
+ I18n.t("forms.record_types.#{record.class.parent_form}", locale:)
+ end
+
+ def form_section_name_translated
+ @form_section_name_translated ||= I18n.with_locale(locale) { form_section&.name }
+ end
+
+ def send_notification?
+ # We may want to add some checks here if users can opt out of particular
+ # types of notifications in the future.
+ true
+ end
+
+ # This is for the message structure in push notifications
+ alias type form_section_name_translated
+
+ def subject
+ I18n.t(
+ 'email_notification.alert_subject',
+ record_type: record_type_translated,
+ id: record.short_id,
+ form_name: form_section_name_translated,
+ locale:
+ )
+ end
+
+ private
+
+ def log_not_found(type, id)
+ Rails.logger.error(
+ "Notification not sent. #{type.capitalize} #{id} not found."
+ )
+ end
+end
diff --git a/app/views/api/v2/alerts/_alert.json.jbuilder b/app/views/api/v2/alerts/_alert.json.jbuilder
index 4219954890..fc59111c4c 100644
--- a/app/views/api/v2/alerts/_alert.json.jbuilder
+++ b/app/views/api/v2/alerts/_alert.json.jbuilder
@@ -6,3 +6,4 @@ json.alert_for alert.alert_for
json.type alert.type
json.date(alert.date&.iso8601)
json.form_unique_id alert.form_sidebar_id
+json.unique_id alert.unique_id
diff --git a/app/views/record_action_mailer/alert_notify.html.erb b/app/views/record_action_mailer/alert_notify.html.erb
new file mode 100644
index 0000000000..6430a5b79d
--- /dev/null
+++ b/app/views/record_action_mailer/alert_notify.html.erb
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ <%= t("email_notification.alert",
+ record_type: @alert_notification.record_type_translated,
+ id: link_to(@alert_notification.record.short_id, url_for_v2(@alert_notification.record)),
+ form_name: @alert_notification.form_section_name_translated,
+ locale: @alert_notification.locale
+ ).html_safe %>
+
+
+
+
\ No newline at end of file
diff --git a/app/views/record_action_mailer/alert_notify.text.erb b/app/views/record_action_mailer/alert_notify.text.erb
new file mode 100644
index 0000000000..2f0d039bca
--- /dev/null
+++ b/app/views/record_action_mailer/alert_notify.text.erb
@@ -0,0 +1,8 @@
+<%= t("email_notification.alert",
+ record_type: @alert_notification.record_type_translated,
+ id: @alert_notification.record.short_id,
+ form_name: @alert_notification.form_section_name_translated,
+ locale: @alert_notification.locale
+)%>
+
+<%= url_for_v2(@alert_notification.record) %>
\ No newline at end of file
diff --git a/app/webpush_notifiers/record_action_webpush_notifier.rb b/app/webpush_notifiers/record_action_webpush_notifier.rb
index 58ea5fa41a..9d5e748d47 100644
--- a/app/webpush_notifiers/record_action_webpush_notifier.rb
+++ b/app/webpush_notifiers/record_action_webpush_notifier.rb
@@ -18,6 +18,10 @@ def self.manager_approval_response(approval_notification)
RecordActionWebpushNotifier.new.manager_approval_response(approval_notification)
end
+ def self.alert_notify(alert_notification)
+ RecordActionWebpushNotifier.new.alert_notify(alert_notification)
+ end
+
def self.transfer_request(transfer_request_notification)
RecordActionWebpushNotifier.new.transfer_request(transfer_request_notification)
end
@@ -62,6 +66,16 @@ def transfer_request(transfer_request_notification)
)
end
+ def alert_notify(alert_notification)
+ return unless alert_notification.send_notification?
+ return unless webpush_notifications_enabled?(alert_notification.user)
+
+ WebpushService.send_notifications(
+ alert_notification.user,
+ message_structure(alert_notification)
+ )
+ end
+
def message_structure(record_action_notification)
{
title: I18n.t("webpush_notification.#{record_action_notification.key}.title"),
diff --git a/config/locales/en.yml b/config/locales/en.yml
index a4aa750883..45a273ff20 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -841,6 +841,8 @@ en:
assign_html: '%{user} has assigned the following %{record_type} to you: %{record_id}.'
transfer_request_html: 'Primero user %{user} from %{agency} is requesting that you transfer ownership of this %{record_type} record %{record_id} so that they can provide %{record_type} management services for the individual in their area. If this transfer is acceptable to you, please click on the %{record_type} ID link in this email to open the %{record_type} in Primero and initiate the transfer.'
transfer_request_notes_html: 'Notes from the individual making the request: %{request_transfer_notes}'
+ alert: '%{record_type} %{id} - %{form_name} has been updated. Please log in to Primero to review the changes.'
+ alert_subject: '%{record_type}: %{id} - %{form_name} Updated'
webpush_notification:
action_label: Go to Case
approval_request:
@@ -858,6 +860,9 @@ en:
transfer:
title: New Transfer
body: You have received a new Case Transfer.
+ alert_case:
+ title: Case Updated
+ body: "%{type} has been updated on one of your cases."
transfer_request:
title: Transfer Request
body: You have received a new Case Transfer Request
@@ -2375,6 +2380,7 @@ en:
workflow_team: Workflow - Teams cases
write: Write
activity_log: Activity Log
+ remove_alert: Remove Alert
resource:
kpi:
actions:
@@ -2688,6 +2694,12 @@ en:
that this permission also allows the user to see a link to the incident
which was created.
label: Create an incident from a case
+ remove_alert:
+ explanation: This permission allows the user to remove an alert from
+ a case. Alerts are typically generated when a particular field is
+ edited, such as when notes are updated. Alerts are usually shown
+ with a yellow dot next to the case.
+ label: Remove alert from a case
manage:
explanation: Allows a user to perform all available actions on cases.
The "Manage" permission essentially means "everything".
@@ -3217,6 +3229,12 @@ en:
skills and an in-depth knowledge of how data is formatted in Primero.
Only performed from incident list page.
label: Import
+ remove_alert:
+ explanation: This permission allows the user to remove an alert from
+ an incident. Alerts are typically generated when a particular field is
+ edited, such as when notes are updated. Alerts are usually shown
+ with a yellow dot next to the incident.
+ label: Remove alert from an incident
manage:
explanation: Allows a user to perform all available actions on incidents.
The "Manage" permission essentially means "everything".
diff --git a/config/routes.rb b/config/routes.rb
index 72d540cdb4..e2b4b78fec 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -39,7 +39,7 @@
resources :children, as: :cases, path: :cases do
resources :children_incidents, as: :incidents, path: :incidents, only: %i[index new]
resources :flags, only: %i[index create update]
- resources :alerts, only: [:index]
+ resources :alerts, only: %i[index destroy]
resources :assigns, only: %i[index create]
resources :referrals, only: %i[index create destroy update]
resources :transfers, only: %i[index create update]
@@ -62,7 +62,7 @@
resources :incidents do
resources :flags, only: %i[index create update]
- resources :alerts, only: [:index]
+ resources :alerts, only: %i[index destroy]
resources :approvals, only: [:update]
resources :attachments, only: %i[create destroy]
post :flags, to: 'flags#create_bulk', on: :collection
diff --git a/db/migrate/20230921124122_add_send_email_to_alert.rb b/db/migrate/20230921124122_add_send_email_to_alert.rb
new file mode 100644
index 0000000000..c9dbf974c3
--- /dev/null
+++ b/db/migrate/20230921124122_add_send_email_to_alert.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSendEmailToAlert < ActiveRecord::Migration[6.1]
+ def change
+ add_column :alerts, :send_email, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9eff04d456..5fa1e2c863 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_09_20_000000) do
+ActiveRecord::Schema.define(version: 2023_09_21_124122) do
# These are extensions that must be enabled in order to support this database
enable_extension "ltree"
@@ -83,6 +83,7 @@
t.integer "agency_id"
t.string "record_type"
t.uuid "record_id"
+ t.boolean "send_email", default: false
t.index ["agency_id"], name: "index_alerts_on_agency_id"
t.index ["record_type", "record_id"], name: "index_alerts_on_record_type_and_record_id"
t.index ["user_id"], name: "index_alerts_on_user_id"
diff --git a/spec/jobs/alert_notify_spec.rb b/spec/jobs/alert_notify_spec.rb
new file mode 100644
index 0000000000..f0ae8cd3e9
--- /dev/null
+++ b/spec/jobs/alert_notify_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AlertNotifyJob, type: :job do
+ include ActiveJob::TestHelper
+
+ before do
+ clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, UserGroup, Agency, Alert)
+ role = create :role
+ @owner = create :user, role:, user_name: 'owner', full_name: 'Owner', email: 'owner@primero.dev'
+ @provider = create :user, role:, user_name: 'provider', full_name: 'Provider', email: 'provider@primero.dev'
+ @other = create :user, role:, user_name: 'other', full_name: 'Other', email: 'other@primero.dev'
+ @child = Child.new(
+ owned_by: @owner.user_name,
+ name: 'child',
+ module_id: PrimeroModule::CP,
+ case_id_display: '12345',
+ consent_for_services: true,
+ disclosure_other_orgs: true
+ )
+ @child.save!
+ @child.assigned_user_names = [@provider.user_name]
+ @child.save!
+ # Force an update on associated_users
+ @child.associated_users(true)
+ end
+
+ describe 'perform_later' do
+ it 'enqueues a job when an alert is created' do
+ ActiveJob::Base.queue_adapter = :test
+ @child.last_updated_by = @other.user_name
+ @child.update_history
+ @child.save!
+ alert = Alert.new(send_email: true, alert_for: Alertable::FIELD_CHANGE, record: @child, type: 'some_type')
+ alert.save!
+ alert_notify_jobs = enqueued_jobs.select { |j| j[:job] == AlertNotifyJob }
+ # There should be two jobs, one for the owner and one for the provider
+ expect(alert_notify_jobs.size).to eq(2)
+ end
+ it 'sends two notifications' do
+ @child.last_updated_by = @other.user_name
+ @child.update_history
+ @child.save!
+ alert = Alert.new(send_email: true, alert_for: Alertable::FIELD_CHANGE, record: @child, type: 'some_type')
+ alert.save!
+ perform_enqueued_jobs(only: AlertNotifyJob)
+ expect(ActionMailer::Base.deliveries.size).to eq(2)
+ end
+ it 'does not send a notification to the person who made the change' do
+ @child.last_updated_by = @provider.user_name
+ @child.update_history
+ @child.save!
+ alert = Alert.new(send_email: true, alert_for: Alertable::FIELD_CHANGE, record: @child, type: 'some_type')
+ alert.save!
+ perform_enqueued_jobs(only: AlertNotifyJob)
+ expect(ActionMailer::Base.deliveries.size).to eq(1)
+ end
+ end
+
+ after :each do
+ clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, UserGroup, Agency, Alert)
+ end
+
+ private
+
+ def child_with_created_by(created_by, options = {})
+ user = User.new(user_name: created_by)
+ child = Child.new_with_user user, options
+ child.save && child
+ end
+end
diff --git a/spec/mailers/record_action_mailer_spec.rb b/spec/mailers/record_action_mailer_spec.rb
index bfd0e8df48..9f5974482e 100644
--- a/spec/mailers/record_action_mailer_spec.rb
+++ b/spec/mailers/record_action_mailer_spec.rb
@@ -7,7 +7,13 @@
describe RecordActionMailer, type: :mailer do
before do
clean_data(SystemSettings)
- SystemSettings.create(default_locale: 'en', unhcr_needs_codes_mapping: {}, changes_field_to_form: {})
+ SystemSettings.create(default_locale: 'en', unhcr_needs_codes_mapping: {},
+ changes_field_to_form: {
+ 'email_alertable_field' => {
+ form_section_unique_id: 'some_formsection_name',
+ alert_strategy: Alertable::AlertStrategy::ASSOCIATED_USERS
+ }
+ })
end
describe 'approvals' do
@@ -397,8 +403,43 @@
end
end
- after do
- clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, Lookup, UserGroup, Agency, Transition)
+ describe 'Emailable Alert' do
+ before do
+ clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, UserGroup, Agency)
+ FormSection.create!(unique_id: 'some_formsection_name', name: 'some_formsection_name',
+ name_en: 'Form Section Name', name_fr: 'Nom de la section du formulaire')
+ @owner = create :user, user_name: 'owner', full_name: 'Owner', email: 'owner@primero.dev'
+ @provider = create :user, user_name: 'provider', full_name: 'Provider', email: 'provider@primero.dev'
+ @child = Child.new_with_user(@owner, { name: 'child', module_id: PrimeroModule::CP, case_id_display: '12345' })
+ @child.save!
+ @child.assigned_user_names = [@provider.user_name]
+ @child.save!
+ @child.associated_users(true)
+ @child.data = { 'email_alertable_field' => 'some_value' }
+ @child.save!
+ @alert_notification = AlertNotificationService.new(@child.id, @child.alerts.first.id, @owner.user_name)
+ end
+
+ let(:mail) { RecordActionMailer.alert_notify(@alert_notification) }
+
+ describe 'alert' do
+ it 'renders the headers' do
+ expect(mail.subject).to eq("Case: #{@child.short_id} - Form Section Name Updated")
+ expect(mail.to).to eq(['owner@primero.dev'])
+ end
+
+ it 'renders the body' do
+ expect(mail.text_part.body.encoded).to match(
+ "Case #{@child.short_id} - Form Section Name has been updated. " \
+ 'Please log in to Primero to review the changes.'
+ )
+ end
+ end
+
+ after do
+ clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, Lookup, UserGroup, Agency, Transition,
+ Alert, Child)
+ end
end
private
diff --git a/spec/models/alert_spec.rb b/spec/models/alert_spec.rb
index 0c3d4624a2..bbbbd794d3 100644
--- a/spec/models/alert_spec.rb
+++ b/spec/models/alert_spec.rb
@@ -23,4 +23,41 @@
expect(Alert.all.map { |alert| alert.record.id }).to match_array([@child2.id, @child3.id])
end
end
+
+ describe 'email alerts' do
+ before :each do
+ clean_data(User, Role, PrimeroModule, PrimeroProgram, Field, FormSection, UserGroup, Agency, Alert,
+ SystemSettings, Child)
+ ss = SystemSettings.create!
+ ss.changes_field_to_form = {
+ 'email_alertable_field' => {
+ form_section_unique_id: 'some_formsection_name1',
+ alert_strategy: Alertable::AlertStrategy::ASSOCIATED_USERS
+ }
+ }
+ ss.save!
+ @owner = create :user, user_name: 'owner', full_name: 'Owner', email: 'owner@primero.dev'
+ @provider = create :user, user_name: 'provider', full_name: 'Provider', email: 'provider@primero.dev'
+ end
+ it 'creates an email alert' do
+ child = Child.new(data: { 'email_alertable_field' => 'some_value' })
+ child.save!
+ expect(Alert.count).to eq(1)
+ end
+ it 'does not create an email alert on other fields' do
+ child = Child.new(data: { 'some_other_field' => 'some_value' })
+ child.save!
+ expect(Alert.count).to eq(0)
+ end
+ it 'deletes the old alert when a duplicate alert is created' do
+ child = Child.new(data: { 'email_alertable_field' => 'some_value' })
+ child.save!
+ expect(Alert.count).to eq(1)
+ old_alert = Alert.first
+ child.data['email_alertable_field'] = 'some_other_value'
+ child.save!
+ expect(Alert.count).to eq(1)
+ expect(Alert.first.unique_id).not_to eq(old_alert.unique_id)
+ end
+ end
end
diff --git a/spec/requests/api/v2/alerts_controller_spec.rb b/spec/requests/api/v2/alerts_controller_spec.rb
index 16768a0b41..1adb8e6d5f 100644
--- a/spec/requests/api/v2/alerts_controller_spec.rb
+++ b/spec/requests/api/v2/alerts_controller_spec.rb
@@ -199,6 +199,67 @@
end
end
+ describe 'DELETE /api/v2//alerts/' do
+ it 'deletes an alert from a child' do
+ alert = @test_child.alerts.first
+ login_for_test(
+ permissions: [
+ Permission.new(
+ resource: Permission::CASE,
+ actions: [Permission::REMOVE_ALERT]
+ )
+ ]
+ )
+
+ delete "/api/v2/cases/#{@test_child.id}/alerts/#{alert.unique_id}"
+ expect(response).to have_http_status(204)
+ expect(@test_child.alerts.count).to eq(2)
+ end
+
+ it 'deletes an alert from a incident' do
+ alert = @test_incident.alerts.first
+ login_for_test(
+ permissions: [
+ Permission.new(
+ resource: Permission::INCIDENT,
+ actions: [Permission::REMOVE_ALERT]
+ )
+ ]
+ )
+ delete "/api/v2/incidents/#{@test_incident.id}/alerts/#{alert.unique_id}"
+ expect(response).to have_http_status(204)
+ expect(@test_incident.alerts.count).to eq(2)
+ end
+ it 'does not delete an alert from a incident if the user does not have remove_alert permission' do
+ alert = @test_incident.alerts.first
+ login_for_test(
+ permissions: [
+ Permission.new(
+ resource: Permission::INCIDENT,
+ actions: [Permission::READ]
+ )
+ ]
+ )
+ delete "/api/v2/incidents/#{@test_incident.id}/alerts/#{alert.unique_id}"
+ expect(response).to have_http_status(403)
+ expect(@test_incident.alerts.count).to eq(3)
+ end
+ it 'does not delete an alert if the alert id does not correspond to the record' do
+ alert = @test_incident.alerts.first
+ login_for_test(
+ permissions: [
+ Permission.new(
+ resource: Permission::CASE,
+ actions: [Permission::REMOVE_ALERT]
+ )
+ ]
+ )
+ delete "/api/v2/cases/#{@test_child.id}/alerts/#{alert.unique_id}"
+ expect(response).to have_http_status(404)
+ expect(@test_incident.alerts.count).to eq(3)
+ end
+ end
+
after do
clear_enqueued_jobs
clean_data(Alert, User, Incident, TracingRequest, Child, Role, Agency)