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