Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rapidpro Connector #474

Draft
wants to merge 15 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/controllers/api/v2/messages_controller.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions app/javascript/components/pages/admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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
}
};
};
13 changes: 13 additions & 0 deletions app/javascript/components/pages/admin/messages-list/actions.js
Original file line number Diff line number Diff line change
@@ -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",
]);
55 changes: 55 additions & 0 deletions app/javascript/components/pages/admin/messages-list/component.jsx
Original file line number Diff line number Diff line change
@@ -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 = (
<ActionButton
icon={<AddIcon />}
text="buttons.new"
type={ACTION_BUTTON_TYPES.default}
onClick={() => setOpen(true)}
/>
);
return (
<Permission resources={RESOURCES.metadata} actions={MANAGE} redirect>
<PageHeading title={i18n.t("settings.navigation.messages")}>{newMessageButton}</PageHeading>
<MessageDialog open={open} onClose={closeDialog} />
<PageContent>
<IndexTable title={i18n.t("settings.navigation.messages")} {...tableOptions} />
</PageContent>
</Permission>
);
}

Component.propTypes = {};
Component.displayName = "MessagesList";

export default Component;
3 changes: 3 additions & 0 deletions app/javascript/components/pages/admin/messages-list/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from "./component";
export { default as NAMESPACE } from "./namespace";
export { default as reducer } from "./reducer";
Original file line number Diff line number Diff line change
@@ -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 (
<ActionDialog dialogTitle={i18n.t("rp_messages.send_message")} open={open} cancelHandler={onClose} confirmButtonLabel={i18n.t("buttons.save")} confirmButtonProps={{form: FORM_ID, type: "submit"}}>
<Form formID={FORM_ID} formSections={form(i18n)} onSubmit={handleSubmit}/>
</ActionDialog>
);
}

// TODO proptypes
Component.propTypes = { open: PropTypes.bool, onClose: PropTypes.func, submit: PropTypes.func };
Component.displayName = "MessageDialog";

export default Component;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./component";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "messages";
19 changes: 19 additions & 0 deletions app/javascript/components/pages/admin/messages-list/reducer.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const RESOURCES = [
"activity_log",
"matching_configuration",
"duplicate",
"kpi"
"kpi",
"message",
];

export const ROLES_PERMISSIONS = Object.freeze({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/components/pages/admin/users-form/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/javascript/components/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export {
ConfigurationsList,
ConfigurationsForm,
LocationsList,
MessagesList,
CodeOfConduct
} from "./admin";
8 changes: 5 additions & 3 deletions app/javascript/components/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand All @@ -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];
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/components/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export {
VIEW_KPIS,
WRITE_RECORDS,
WRITE_REGISTRY_RECORD,
REMOVE_ALERT
REMOVE_ALERT,

} from "./constants";

export { checkPermissions } from "./utils";
Expand Down
12 changes: 10 additions & 2 deletions app/javascript/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
];

Expand Down
4 changes: 3 additions & 1 deletion app/javascript/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion app/javascript/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
UsersList,
ConfigurationsList,
ConfigurationsForm,
MessagesList,
CodeOfConduct as AdminCodeOfConduct,
Support
} from "./components/pages";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -495,7 +501,7 @@ export default [
path: ROUTES.locations,
component: LocationsList,
resources: RESOURCES.locations
}
},
]
}
},
Expand Down
12 changes: 12 additions & 0 deletions app/jobs/send_rapidpro_messages_job.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading