diff --git a/app/controllers/api/v2/messages_controller.rb b/app/controllers/api/v2/messages_controller.rb
new file mode 100644
index 0000000000..7b125cbfe1
--- /dev/null
+++ b/app/controllers/api/v2/messages_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Api::V2::MessagesController < ApplicationApiController
+ include Api::V2::Concerns::Pagination
+ include Api::V2::Concerns::JsonValidateParams
+ def index
+ # TODO: authz
+ @messages = Message.all
+ end
+
+ def create
+ permitted = params.permit(:body, recipient_groups: [])
+ @message = Message.new(permitted)
+ @message.save
+ # status = params[:data][:id].present? ? 204 : 200
+ status = 204
+ render :create, status:
+ end
+end
diff --git a/app/javascript/components/pages/admin/index.js b/app/javascript/components/pages/admin/index.js
index 4fb615e75f..9df400202f 100644
--- a/app/javascript/components/pages/admin/index.js
+++ b/app/javascript/components/pages/admin/index.js
@@ -18,4 +18,5 @@ export { default as AuditLogs } from "./audit-logs";
export { default as ConfigurationsList } from "./configurations-list";
export { default as ConfigurationsForm } from "./configurations-form";
export { default as LocationsList } from "./locations-list";
+export { default as MessagesList } from "./messages-list";
export { default as CodeOfConduct } from "./code-of-conduct";
diff --git a/app/javascript/components/pages/admin/messages-list/action-creators.js b/app/javascript/components/pages/admin/messages-list/action-creators.js
new file mode 100644
index 0000000000..969e4a72a9
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/action-creators.js
@@ -0,0 +1,25 @@
+import { RECORD_PATH } from "../../../../config";
+import actions from "./actions";
+export const fetchMessages = params => {
+ const { data } = params || {};
+
+ return {
+ type: actions.FETCH_MESSAGES,
+ api: {
+ path: RECORD_PATH.messages,
+ params: data
+ }
+ };
+};
+
+// TODO make this handle updates (if we want that as functionality)
+export const saveMessage = ({message}) => {
+ return {
+ type: actions.SAVE_MESSAGE,
+ api: {
+ path: RECORD_PATH.messages,
+ method: "POST",
+ body: message
+ }
+ };
+};
diff --git a/app/javascript/components/pages/admin/messages-list/actions.js b/app/javascript/components/pages/admin/messages-list/actions.js
new file mode 100644
index 0000000000..7a784efa82
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/actions.js
@@ -0,0 +1,13 @@
+import { namespaceActions } from "../../../../libs";
+import namespace from "./namespace";
+
+export default namespaceActions(namespace, [
+ "FETCH_MESSAGES",
+ "FETCH_MESSAGES_STARTED",
+ "FETCH_MESSAGES_SUCCESS",
+ "FETCH_MESSAGES_FAILURE",
+ "FETCH_MESSAGES_FINISHED",
+ "SAVE_MESSAGE",
+ "SAVE_MESSAGE_SUCCESS",
+ "SAVE_MESSAGE_FAILURE",
+]);
\ No newline at end of file
diff --git a/app/javascript/components/pages/admin/messages-list/component.jsx b/app/javascript/components/pages/admin/messages-list/component.jsx
new file mode 100644
index 0000000000..0b6d52fc32
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/component.jsx
@@ -0,0 +1,55 @@
+import PageContent from "../../../page/components/page-content";
+import PageHeading from "../../../page/components/page-heading";
+import Permission, { MANAGE, RESOURCES } from "../../../permissions";
+import IndexTable from "../../../index-table";
+import { useI18n } from "../../../i18n";
+import { default as NAMESPACE } from "./namespace";
+import { fetchMessages } from "./action-creators";
+import ActionButton, { ACTION_BUTTON_TYPES } from "../../../action-button";
+import AddIcon from "@mui/icons-material/Add";
+import { useState } from "react";
+import MessageDialog from "./message-dialog";
+
+function Component() {
+ const i18n = useI18n();
+
+ const [open, setOpen] = useState(false);
+
+ const tableOptions = {
+ recordType: ["admin", NAMESPACE],
+ columns: [
+ { name: "created_at", label: i18n.t("rp_messages.attributes.created_at") },
+ { name: "body", label: i18n.t("rp_messages.attributes.body") }
+ // performed by
+ // recipients
+ ],
+ onTableChange: fetchMessages
+ };
+
+ const closeDialog = () => {
+ setOpen(false);
+ };
+
+ const newMessageButton = (
+ }
+ text="buttons.new"
+ type={ACTION_BUTTON_TYPES.default}
+ onClick={() => setOpen(true)}
+ />
+ );
+ return (
+
+ {newMessageButton}
+
+
+
+
+
+ );
+}
+
+Component.propTypes = {};
+Component.displayName = "MessagesList";
+
+export default Component;
diff --git a/app/javascript/components/pages/admin/messages-list/index.js b/app/javascript/components/pages/admin/messages-list/index.js
new file mode 100644
index 0000000000..aa06c80c94
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/index.js
@@ -0,0 +1,3 @@
+export { default } from "./component";
+export { default as NAMESPACE } from "./namespace";
+export { default as reducer } from "./reducer";
diff --git a/app/javascript/components/pages/admin/messages-list/message-dialog/component.jsx b/app/javascript/components/pages/admin/messages-list/message-dialog/component.jsx
new file mode 100644
index 0000000000..318c3df986
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/message-dialog/component.jsx
@@ -0,0 +1,30 @@
+import PropTypes from "prop-types";
+import ActionDialog from "../../../../action-dialog";
+import Form from "../../../../form";
+import form from "./formSections";
+import { useI18n } from "../../../../i18n";
+import { useDispatch } from "react-redux";
+import { fetchMessages, saveMessage } from "../action-creators";
+
+const FORM_ID = "messages-form";
+
+function Component({ open, onClose, submit }) {
+ const i18n = useI18n();
+ const dispatch = useDispatch();
+
+ const handleSubmit = (message) => {
+ dispatch(saveMessage({message}))
+ onClose();
+ }
+ return (
+
+
+ );
+}
+
+// TODO proptypes
+Component.propTypes = { open: PropTypes.bool, onClose: PropTypes.func, submit: PropTypes.func };
+Component.displayName = "MessageDialog";
+
+export default Component;
diff --git a/app/javascript/components/pages/admin/messages-list/message-dialog/formSections.js b/app/javascript/components/pages/admin/messages-list/message-dialog/formSections.js
new file mode 100644
index 0000000000..39a3a294b3
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/message-dialog/formSections.js
@@ -0,0 +1,27 @@
+import { fromJS } from "immutable";
+import { FieldRecord, FormSectionRecord, OPTION_TYPES, SELECT_FIELD, TEXT_AREA, TEXT_FIELD } from "../../../../form";
+
+const form = i18n =>
+ fromJS([
+ FormSectionRecord({
+ unique_id: "message",
+ fields: [
+ FieldRecord({
+ name: "recipient_groups",
+ display_name: i18n.t("rp_messages.attributes.recipient_groups"),
+ required: true,
+ autoFocus: true,
+ type: SELECT_FIELD,
+ multi_select: true,
+ option_strings_source: OPTION_TYPES.USER_GROUP
+ }),
+ FieldRecord({
+ name: "body",
+ display_name: i18n.t("rp_messages.attributes.body"),
+ required: true,
+ type: TEXT_AREA
+ })
+ ]
+ })
+ ]);
+export default form;
diff --git a/app/javascript/components/pages/admin/messages-list/message-dialog/index.js b/app/javascript/components/pages/admin/messages-list/message-dialog/index.js
new file mode 100644
index 0000000000..b6e0586481
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/message-dialog/index.js
@@ -0,0 +1 @@
+export { default } from "./component";
diff --git a/app/javascript/components/pages/admin/messages-list/namespace.js b/app/javascript/components/pages/admin/messages-list/namespace.js
new file mode 100644
index 0000000000..aab483edcd
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/namespace.js
@@ -0,0 +1 @@
+export default "messages";
\ No newline at end of file
diff --git a/app/javascript/components/pages/admin/messages-list/reducer.js b/app/javascript/components/pages/admin/messages-list/reducer.js
new file mode 100644
index 0000000000..648b94fadf
--- /dev/null
+++ b/app/javascript/components/pages/admin/messages-list/reducer.js
@@ -0,0 +1,19 @@
+import { fromJS } from "immutable";
+import actions from "./actions";
+
+const DEFAULT_STATE = fromJS({});
+
+export default (state = DEFAULT_STATE, {type, payload}) => {
+ switch (type) {
+ case actions.FETCH_MESSAGES_STARTED:
+ return state.set("loading", fromJS(payload)).set("errors", false);
+ case actions.FETCH_MESSAGES_SUCCESS:
+ return state.set("data", fromJS(payload.data))
+ case actions.FETCH_MESSAGES_FAILURE:
+ return state.set(errors, true).set("loading", false);
+ case actions.FETCH_MESSAGES_FINISHED:
+ return state.set("errors", false).set("loading", false);
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/components/pages/admin/roles-form/constants.js b/app/javascript/components/pages/admin/roles-form/constants.js
index 44243b4483..c888213809 100644
--- a/app/javascript/components/pages/admin/roles-form/constants.js
+++ b/app/javascript/components/pages/admin/roles-form/constants.js
@@ -38,7 +38,8 @@ export const RESOURCES = [
"activity_log",
"matching_configuration",
"duplicate",
- "kpi"
+ "kpi",
+ "message",
];
export const ROLES_PERMISSIONS = Object.freeze({
diff --git a/app/javascript/components/pages/admin/users-form/constants.js b/app/javascript/components/pages/admin/users-form/constants.js
index b29d00241a..76df389b88 100644
--- a/app/javascript/components/pages/admin/users-form/constants.js
+++ b/app/javascript/components/pages/admin/users-form/constants.js
@@ -78,7 +78,9 @@ export const FIELD_NAMES = Object.freeze({
RECEIVE_WEBPUSH_APPROVAL_REQUEST: "settings.notifications.receive_webpush.approval_request",
RECEIVE_WEBPUSH_APPROVAL_RESPONSE: "settings.notifications.receive_webpush.approval_response",
RECEIVE_WEBPUSH_TRANSITION_NOTIFICATION: "settings.notifications.receive_webpush.transition_notification",
- RECEIVE_WEBPUSH_TRANSFER_REQUEST: "settings.notifications.receive_webpush.transfer_request"
+ RECEIVE_WEBPUSH_TRANSFER_REQUEST: "settings.notifications.receive_webpush.transfer_request",
+ RAPIDPRO_URN: "rapidpro_urn",
+ RECEIVE_MESSAGES: "receive_messages"
});
export const NOTIFICATIONS_PREFERENCES = [
diff --git a/app/javascript/components/pages/admin/users-form/form.js b/app/javascript/components/pages/admin/users-form/form.js
index 308b6a5d40..1c74eca262 100644
--- a/app/javascript/components/pages/admin/users-form/form.js
+++ b/app/javascript/components/pages/admin/users-form/form.js
@@ -204,6 +204,11 @@ const sharedUserFields = (
option_strings_source: OPTION_TYPES.LOCATION,
required: true
},
+ {
+ display_name: i18n.t("user.rapidpro_urn"),
+ name: FIELD_NAMES.RAPIDPRO_URN,
+ type: TEXT_FIELD,
+ },
{
display_name: i18n.t("user.disabled"),
name: FIELD_NAMES.DISABLED,
diff --git a/app/javascript/components/pages/index.js b/app/javascript/components/pages/index.js
index 327c962d29..a8a7b7af75 100644
--- a/app/javascript/components/pages/index.js
+++ b/app/javascript/components/pages/index.js
@@ -25,5 +25,6 @@ export {
ConfigurationsList,
ConfigurationsForm,
LocationsList,
+ MessagesList,
CodeOfConduct
} from "./admin";
diff --git a/app/javascript/components/permissions/constants.js b/app/javascript/components/permissions/constants.js
index e2160bbc3a..7f6695efd6 100644
--- a/app/javascript/components/permissions/constants.js
+++ b/app/javascript/components/permissions/constants.js
@@ -120,7 +120,7 @@ export const ACTIONS = {
WRITE: "write",
VIEW_FAMILY_RECORD: "view_family_record",
LINK_FAMILY_RECORD: "link_family_record",
- REMOVE_ALERT: "remove_alert"
+ REMOVE_ALERT: "remove_alert",
};
export const MANAGE = [ACTIONS.MANAGE];
@@ -151,7 +151,8 @@ export const RESOURCES = {
tracing_requests: "tracing_requests",
user_groups: "user_groups",
users: "users",
- webhooks: "webhooks"
+ webhooks: "webhooks",
+ messages: "messages",
};
export const RECORD_RESOURCES = [RESOURCES.cases, RESOURCES.incidents, RESOURCES.tracing_requests];
@@ -166,7 +167,8 @@ export const ADMIN_RESOURCES = [
RESOURCES.forms,
RESOURCES.metadata,
RESOURCES.audit_logs,
- RESOURCES.webhooks
+ RESOURCES.webhooks,
+ RESOURCES.messages
];
export const SEARCH_OTHERS = [...MANAGE, ACTIONS.SEARCH_OWNED_BY_OTHERS];
diff --git a/app/javascript/components/permissions/index.js b/app/javascript/components/permissions/index.js
index 2b06ff994b..5e07df8813 100644
--- a/app/javascript/components/permissions/index.js
+++ b/app/javascript/components/permissions/index.js
@@ -52,7 +52,8 @@ export {
VIEW_KPIS,
WRITE_RECORDS,
WRITE_REGISTRY_RECORD,
- REMOVE_ALERT
+ REMOVE_ALERT,
+
} from "./constants";
export { checkPermissions } from "./utils";
diff --git a/app/javascript/config.js b/app/javascript/config.js
index 8cec2a2f22..5ea65c95f9 100644
--- a/app/javascript/config.js
+++ b/app/javascript/config.js
@@ -19,7 +19,7 @@ import {
SHOW_SUMMARY,
READ_MANAGED_REPORTS,
READ_REGISTRY_RECORD,
- READ_FAMILY_RECORD
+ READ_FAMILY_RECORD,
} from "./components/permissions/constants";
import getAdminResources from "./components/pages/admin/utils/get-admin-resources";
@@ -137,7 +137,8 @@ const RECORD_PATH = {
users: "users",
activity_log: "activity_log",
registry_records: "registry_records",
- webpush_config: "webpush/config"
+ webpush_config: "webpush/config",
+ messages: "messages"
};
const RECORD_INFORMATION_GROUP = "record_information";
@@ -181,6 +182,7 @@ const ROUTES = {
admin_users: "/admin/users",
admin_users_new: "/admin/users/new",
audit_logs: "/admin/audit_logs",
+ admin_messages: "/admin/messages",
cases: "/cases",
configurations: "/admin/configurations",
admin_configurations_new: "/admin/configurations/new",
@@ -353,6 +355,12 @@ const ADMIN_NAV = [
label: "settings.navigation.audit_logs",
permission: SHOW_AUDIT_LOGS,
recordType: RESOURCES.audit_logs
+ },
+ {
+ to: "/messages",
+ label: "settings.navigation.messages",
+ permission: MANAGE,
+ recordType: RESOURCES.messages
}
];
diff --git a/app/javascript/reducer.js b/app/javascript/reducer.js
index 21a6b4a5b5..85ddab49ac 100644
--- a/app/javascript/reducer.js
+++ b/app/javascript/reducer.js
@@ -52,6 +52,7 @@ import { reducer as kpiReducer } from "./components/key-performance-indicators";
import { reducer as configurationsListReducer } from "./components/pages/admin/configurations-list";
import { reducer as configurationsFormReducer } from "./components/pages/admin/configurations-form";
import { reducer as activityLogReducer } from "./components/activity-log";
+import { reducer as messageListReducer} from "./components/pages/admin/messages-list";
import {
reducer as locationsListReducer,
importReducer as locationsImportReducer
@@ -115,7 +116,8 @@ const rootReducer = {
roles: reduceReducers(initialState, rolesListReducer, rolesFormReducer),
lookups: reduceReducers(initialState, lookupsListReducer, AdminLookupsFormReducers),
configurations: reduceReducers(initialState, configurationsListReducer, configurationsFormReducer),
- locations: reduceReducers(initialState, locationsListReducer, locationsImportReducer)
+ locations: reduceReducers(initialState, locationsListReducer, locationsImportReducer),
+ messages: reduceReducers(initialState, messageListReducer)
}),
activity_logs: activityLogReducer,
...kpiReducer
diff --git a/app/javascript/routes.js b/app/javascript/routes.js
index 6d0c94bfea..8d9ba6d26b 100644
--- a/app/javascript/routes.js
+++ b/app/javascript/routes.js
@@ -25,6 +25,7 @@ import {
UsersList,
ConfigurationsList,
ConfigurationsForm,
+ MessagesList,
CodeOfConduct as AdminCodeOfConduct,
Support
} from "./components/pages";
@@ -449,6 +450,11 @@ export default [
component: RolesList,
resources: RESOURCES.roles
},
+ {
+ path: ROUTES.admin_messages,
+ component: MessagesList,
+ resources: RESOURCES.messages
+ },
{
path: `${ROUTES.forms}/new`,
component: FormBuilder,
@@ -495,7 +501,7 @@ export default [
path: ROUTES.locations,
component: LocationsList,
resources: RESOURCES.locations
- }
+ },
]
}
},
diff --git a/app/jobs/send_rapidpro_messages_job.rb b/app/jobs/send_rapidpro_messages_job.rb
new file mode 100644
index 0000000000..02082e59ac
--- /dev/null
+++ b/app/jobs/send_rapidpro_messages_job.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class SendRapidproMessagesJob < ApplicationJob
+ queue_as :mailer
+ def perform(message_id)
+ message = Message.find(message_id)
+ recipients = message.recipients
+ valid_urns = recipients.where.not(rapidpro_urn: nil).where.not(rapidpro_urn: '').map(&:rapidpro_urn)
+ connector = RapidproConnectorService.instance
+ connector.send_message_bulk(valid_urns, message.body)
+ end
+end
diff --git a/app/models/message.rb b/app/models/message.rb
new file mode 100644
index 0000000000..ae3136174e
--- /dev/null
+++ b/app/models/message.rb
@@ -0,0 +1,24 @@
+class Message < ApplicationRecord
+ has_and_belongs_to_many :recipients, class_name: 'User', join_table: 'messages_recipients'
+ validates :body, presence: true
+
+ attr_writer :recipient_groups
+
+ before_create :materialize_recipients
+
+ after_create :send_message
+
+ def materialize_recipients
+ recipient_users = Set.new
+ @recipient_groups ||= []
+ @recipient_groups.each do |group_uid|
+ group = UserGroup.find_by(unique_id: group_uid)
+ recipient_users.add(group.users)
+ end
+ recipients << recipient_users.to_a
+ end
+
+ def send_message
+ SendRapidproMessagesJob.perform_later(id)
+ end
+end
diff --git a/app/models/permission.rb b/app/models/permission.rb
index 1822224f42..551dc39ce6 100644
--- a/app/models/permission.rb
+++ b/app/models/permission.rb
@@ -183,6 +183,7 @@ class Permission < ValueObject
LINK_INCIDENT_TO_CASE = 'link_incident_to_case'
LINK_FAMILY_RECORD = 'link_family_record'
REMOVE_ALERT = 'remove_alert'
+ MESSAGE = 'message'
RESOURCE_ACTIONS = {
CASE => [
@@ -257,7 +258,8 @@ class Permission < ValueObject
KPI_SUPERVISOR_TO_CASEWORKER_RATIO, KPI_TIME_FROM_CASE_OPEN_TO_CLOSE
],
CODE_OF_CONDUCT => [MANAGE],
- ACTIVITY_LOG => [MANAGE, TRANSFER]
+ ACTIVITY_LOG => [MANAGE, TRANSFER],
+ MESSAGE => [MANAGE]
}.freeze
RESOURCE_FORM_ACTIONS = {
@@ -317,7 +319,7 @@ def actions
def resources
[CASE, INCIDENT, TRACING_REQUEST, POTENTIAL_MATCH, REGISTRY_RECORD, ROLE, USER, USER_GROUP, AGENCY, WEBHOOK,
METADATA, SYSTEM, REPORT, MANAGED_REPORT, DASHBOARD, AUDIT_LOG, MATCHING_CONFIGURATION, DUPLICATE,
- CODE_OF_CONDUCT, ACTIVITY_LOG]
+ CODE_OF_CONDUCT, ACTIVITY_LOG, MESSAGE]
end
def records
diff --git a/app/models/user.rb b/app/models/user.rb
index b058b6d27f..5901c700ac 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -29,6 +29,7 @@ class User < ApplicationRecord
'password_reset' => { 'type' => 'boolean' }, 'role_id' => { 'type' => 'string' },
'agency_office' => { 'type' => %w[string null] }, 'code_of_conduct_id' => { 'type' => 'integer' },
'send_mail' => { 'type' => 'boolean' }, 'receive_webpush' => { 'type' => 'boolean' },
+ 'rapidpro_urn' => { 'type' => 'string' },
'settings' => {
'type' => %w[object null], 'properties' => {
'notifications' => {
@@ -531,6 +532,10 @@ def receive_webpush?
receive_webpush == true && !disabled?
end
+ def messageable?
+ receive_messages == true && !disabled?
+ end
+
def authorized_referral_roles(record)
return Role.none unless record.respond_to?(:referrals_to_user)
diff --git a/app/services/api_connector/abstract_connector.rb b/app/services/api_connector/abstract_connector.rb
index e0f9009abc..08dba93a75 100644
--- a/app/services/api_connector/abstract_connector.rb
+++ b/app/services/api_connector/abstract_connector.rb
@@ -24,7 +24,7 @@ def id
self.class.id
end
- def default_env_prefix
+ def self.default_env_prefix
"PRIMERO_CONNECT_API_#{id.upcase}_"
end
diff --git a/app/services/api_connector/rapidpro_connector.rb b/app/services/api_connector/rapidpro_connector.rb
new file mode 100644
index 0000000000..d33f039eb6
--- /dev/null
+++ b/app/services/api_connector/rapidpro_connector.rb
@@ -0,0 +1,35 @@
+class ApiConnector::RapidproConnector < ApiConnector::AbstractConnector
+ IDENTIFIER = 'rp'
+
+ def self.id
+ IDENTIFIER
+ end
+
+ def self.build_from_env(options = {})
+ prefix = options[:prefix] || default_env_prefix
+ config = ENV.select { |key, _| key.start_with?(prefix) }
+ .transform_keys { |key| key.delete_prefix(prefix).downcase }
+ .with_indifferent_access
+
+ token = config.delete(:token)
+ config['default_headers'] = {
+ Authorization: "Token #{token}"
+ }
+ new(config)
+ end
+
+ def send_message(urn, text)
+ connection.post('/api/v2/broadcasts.json', post_params(urn, text))
+ end
+
+ def send_message_bulk(urns, text)
+ connection.post('/api/v2/broadcasts.json', { urns:, text: })
+ end
+
+ def post_params(urn, text)
+ {
+ urns: [urn],
+ text:
+ }
+ end
+end
diff --git a/app/services/primero_model_service.rb b/app/services/primero_model_service.rb
index db9e299b11..59e0eeb496 100644
--- a/app/services/primero_model_service.rb
+++ b/app/services/primero_model_service.rb
@@ -10,7 +10,7 @@ class PrimeroModelService
ReportableFollowUp ReportableProtectionConcern ReportableService Dashboard
Flag Alert Attachment AuditLog BulkExport RecordHistory SavedSearch Transition Task ActivityLog
Agency ContactInformation Field FormSection Location Lookup PrimeroModule PrimeroProgram Report User Role
- Permission SystemSettings UserGroup ExportConfiguration PrimeroConfiguration Webhook IdentityProvider Kpi
+ Permission SystemSettings UserGroup ExportConfiguration PrimeroConfiguration Webhook IdentityProvider Kpi Message
].freeze
def self.to_model(name)
diff --git a/app/services/rapidpro_connector_service.rb b/app/services/rapidpro_connector_service.rb
new file mode 100644
index 0000000000..c6bea190cf
--- /dev/null
+++ b/app/services/rapidpro_connector_service.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class RapidproConnectorService
+ class << self
+ def instance
+ @instance || ApiConnector::RapidproConnector.build_from_env.freeze
+ end
+ end
+end
diff --git a/app/views/api/v2/messages/_message.json.jbuilder b/app/views/api/v2/messages/_message.json.jbuilder
new file mode 100644
index 0000000000..0be7f95314
--- /dev/null
+++ b/app/views/api/v2/messages/_message.json.jbuilder
@@ -0,0 +1 @@
+json.extract! message, :id, :created_at, :updated_at, :body
diff --git a/app/views/api/v2/messages/create.json.jbuilder b/app/views/api/v2/messages/create.json.jbuilder
new file mode 100644
index 0000000000..5b2cf90dae
--- /dev/null
+++ b/app/views/api/v2/messages/create.json.jbuilder
@@ -0,0 +1,3 @@
+json.data do
+ json.partial! 'api/v2/messages/message', message: @message
+end
diff --git a/app/views/api/v2/messages/index.json.jbuilder b/app/views/api/v2/messages/index.json.jbuilder
new file mode 100644
index 0000000000..9b2b8040f7
--- /dev/null
+++ b/app/views/api/v2/messages/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.data do
+ json.array! @messages, partial: 'api/v2/messages/message', as: :message
+end
diff --git a/app/views/api/v2/messages/show.json.jbuilder b/app/views/api/v2/messages/show.json.jbuilder
new file mode 100644
index 0000000000..a500e40f00
--- /dev/null
+++ b/app/views/api/v2/messages/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! "messages/message", message: @message
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e96122072b..6ecd4f25f5 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -4187,6 +4187,7 @@ en:
users: Users
roles: Roles
code_of_conduct: Code of Conduct
+ messages: Messages
title: Settings
system_settings:
label: System Settings
@@ -4535,3 +4536,9 @@ en:
body:
one: Do you want to allow Primero to send you notifications? Remember that you will continue to receive notifications on this device %{count} day after you log out. This is not recommended for shared devices.
other: Do you want to allow Primero to send you notifications? Remember that you will continue to receive notifications on this device %{count} days after you log out. This is not recommended for shared devices.
+ rp_messages:
+ attributes:
+ created_at: Created At
+ body: Body
+ recipient_groups: Recipient User Groups
+ send_message: Send Message
diff --git a/config/routes.rb b/config/routes.rb
index 1b73c4e2cb..79fd7d74a1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,7 @@
# Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
Rails.application.routes.draw do
+ get 'message/index'
root to: 'home#v2'
scope :v2 do
@@ -137,6 +138,7 @@
resources :key_performance_indicators, path: :kpis, only: [:show]
resources :codes_of_conduct, only: %i[index create], controller: 'codes_of_conduct'
resources :activity_log, only: [:index]
+ resources :messages, only: %i[index create]
resources :managed_reports, only: %i[index show] do
collection do
get :export, to: 'managed_reports#export'
diff --git a/db/migrate/20240926130958_create_message.rb b/db/migrate/20240926130958_create_message.rb
new file mode 100644
index 0000000000..2f010f425a
--- /dev/null
+++ b/db/migrate/20240926130958_create_message.rb
@@ -0,0 +1,8 @@
+class CreateMessage < ActiveRecord::Migration[6.1]
+ def change
+ create_table :messages do |t|
+ t.text :body
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240926133159_add_message_fields_to_user.rb b/db/migrate/20240926133159_add_message_fields_to_user.rb
new file mode 100644
index 0000000000..bd0b352598
--- /dev/null
+++ b/db/migrate/20240926133159_add_message_fields_to_user.rb
@@ -0,0 +1,6 @@
+class AddMessageFieldsToUser < ActiveRecord::Migration[6.1]
+ def change
+ add_column :users, :rapidpro_urn, :text
+ add_column :users, :receive_messages, :boolean
+ end
+end
diff --git a/db/migrate/20241028142603_create_join_table_messages_recipients.rb b/db/migrate/20241028142603_create_join_table_messages_recipients.rb
new file mode 100644
index 0000000000..ccabf1485d
--- /dev/null
+++ b/db/migrate/20241028142603_create_join_table_messages_recipients.rb
@@ -0,0 +1,8 @@
+class CreateJoinTableMessagesRecipients < ActiveRecord::Migration[6.1]
+ def change
+ create_join_table :messages, :users, table_name: 'messages_recipients' do |t|
+ # t.index [:message_id, :user_id]
+ # t.index [:user_id, :message_id]
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c6c1af565c..f29f8a81de 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2024_03_06_154915) do
+ActiveRecord::Schema.define(version: 2024_10_28_142603) do
# These are extensions that must be enabled in order to support this database
enable_extension "ltree"
@@ -387,6 +387,17 @@
t.index ["unique_id"], name: "index_lookups_on_unique_id", unique: true
end
+ create_table "messages", force: :cascade do |t|
+ t.text "body"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "messages_recipients", id: false, force: :cascade do |t|
+ t.bigint "message_id", null: false
+ t.bigint "user_id", null: false
+ end
+
create_table "perpetrators", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.jsonb "data", default: {}
t.index ["data"], name: "index_perpetrators_on_data", using: :gin
@@ -668,6 +679,8 @@
t.bigint "code_of_conduct_id"
t.boolean "receive_webpush"
t.jsonb "settings"
+ t.text "rapidpro_urn"
+ t.boolean "receive_messages"
t.index ["agency_id"], name: "index_users_on_agency_id"
t.index ["code_of_conduct_id"], name: "index_users_on_code_of_conduct_id"
t.index ["email"], name: "index_users_on_email", unique: true
diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb
new file mode 100644
index 0000000000..d74b6a8e20
--- /dev/null
+++ b/spec/models/message_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Message, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/services/api_connector/rapidpro_connector_spec.rb b/spec/services/api_connector/rapidpro_connector_spec.rb
new file mode 100644
index 0000000000..9c48f170c1
--- /dev/null
+++ b/spec/services/api_connector/rapidpro_connector_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe ApiConnector::RapidproConnector do
+ let(:connection) { double('connection') }
+ let(:connector) do
+ c = ApiConnector::RapidproConnector.new
+ (c.connection = connection) && c
+ end
+
+ describe '.send_message' do
+ it 'calls the rapidpro broadcasts endpoint with the desired output' do
+ expect(connection).to(
+ receive(:post).with('/api/v2/broadcasts.json', { text: 'test message', urns: ['tel:+11234561234'] })
+ )
+ connector.send_message('tel:+11234561234', 'test message')
+ end
+ end
+end