Skip to content

Commit

Permalink
Merge pull request #20 from MoveOnOrg/moveon-stage
Browse files Browse the repository at this point in the history
Merge staging into prod post-QA
  • Loading branch information
sjwmoveon authored Jun 6, 2024
2 parents 816fd7d + e49fa33 commit bcaf4d7
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 4 deletions.
102 changes: 102 additions & 0 deletions __test__/server/api/campaign/campaign.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
sleep,
startCampaign
} from "../../../test_helpers";
import { dynamicReassignMutation } from "../../../../src/containers/AssignReplies";

let testAdminUser;
let testInvite;
Expand Down Expand Up @@ -815,6 +816,55 @@ describe("Reassignments", () => {
testTexterUser
);

texterCampaignDataResults2 = await runGql(
TexterTodoQuery,
{
contactsFilter: {
messageStatus: "needsResponse",
isOptedOut: false,
validTimezone: true
},
assignmentId: assignmentId2,
organizationId
},
testTexterUser2
);
// TEXTER 1 (60 needsMessage, 2 needsResponse, 4 messaged)
// TEXTER 2 (25 needsMessage, 3 convo, 1 messaged)
expect(texterCampaignDataResults.data.assignment.contacts.length).toEqual(
2
);
expect(texterCampaignDataResults.data.assignment.allContactsCount).toEqual(
66
);
expect(texterCampaignDataResults2.data.assignment.contacts.length).toEqual(
0
);
expect(texterCampaignDataResults2.data.assignment.allContactsCount).toEqual(
29
);
await runGql(
dynamicReassignMutation,
{
joinToken: testCampaign.joinToken,
campaignId: testCampaign.id,
},
testTexterUser2
);
texterCampaignDataResults = await runGql(
TexterTodoQuery,
{
contactsFilter: {
messageStatus: "needsResponse",
isOptedOut: false,
validTimezone: true
},
assignmentId,
organizationId
},
testTexterUser
);

texterCampaignDataResults2 = await runGql(
TexterTodoQuery,
{
Expand All @@ -840,6 +890,58 @@ describe("Reassignments", () => {
expect(texterCampaignDataResults2.data.assignment.allContactsCount).toEqual(
29
);
jest.useFakeTimers()
jest.advanceTimersByTime(4000000)
await runGql(
dynamicReassignMutation,
{
joinToken: testCampaign.joinToken,
campaignId: testCampaign.id,
},
testTexterUser2
);
jest.useRealTimers()
texterCampaignDataResults = await runGql(
TexterTodoQuery,
{
contactsFilter: {
messageStatus: "needsResponse",
isOptedOut: false,
validTimezone: true
},
assignmentId,
organizationId
},
testTexterUser
);

texterCampaignDataResults2 = await runGql(
TexterTodoQuery,
{
contactsFilter: {
messageStatus: "needsResponse",
isOptedOut: false,
validTimezone: true
},
assignmentId: assignmentId2,
organizationId
},
testTexterUser2
);
// TEXTER 1 (60 needsMessage, 4 messaged)
// TEXTER 2 (25 needsMessage, 2 needsResponse, 3 convo, 1 messaged)
expect(texterCampaignDataResults.data.assignment.contacts.length).toEqual(
0
);
expect(texterCampaignDataResults.data.assignment.allContactsCount).toEqual(
64
);
expect(texterCampaignDataResults2.data.assignment.contacts.length).toEqual(
2
);
expect(texterCampaignDataResults2.data.assignment.allContactsCount).toEqual(
31
);
}, 10000); // long test can exceed default 5seconds
});

Expand Down
1 change: 1 addition & 0 deletions __test__/test_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export async function createCampaign(
const campaignQuery = `mutation createCampaign($input: CampaignInput!) {
createCampaign(campaign: $input) {
id
joinToken
}
}`;
const variables = {
Expand Down
35 changes: 35 additions & 0 deletions migrations/20240503180901_campaigncontactsupdatedat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

const { onUpdateTrigger } = require('./helpers/index')
const ON_UPDATE_TIMESTAMP_FUNCTION = `
CREATE OR REPLACE FUNCTION on_update_timestamp()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
`

const DROP_ON_UPDATE_TIMESTAMP_FUNCTION = `DROP FUNCTION on_update_timestamp`

/**
* @param { import("knex").Knex } knex
*/
exports.up = async function(knex) {
const isSqlite = /sqlite/.test(knex.client.config.client);
if (!isSqlite) {
await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION);
await knex.raw(onUpdateTrigger('campaign_contact'));
}
};

/**
* @param { import("knex").Knex } knex
*/
exports.down = async function(knex) {
const isSqlite = /sqlite/.test(knex.client.config.client);
if (!isSqlite) {
await knex.raw("DROP TRIGGER campaign_contact_updated_at on campaign_contact");
await knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION);
}
};
8 changes: 8 additions & 0 deletions migrations/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ exports.redefineSqliteTable = async (knex, tableName, newTableFn) => {
await knex.schema.dropTable(tableName);
await knex.schema.createTable(tableName, newTableFn);
};


exports.onUpdateTrigger = table => `
CREATE TRIGGER ${table}_updated_at
BEFORE UPDATE ON ${table}
FOR EACH ROW
EXECUTE PROCEDURE on_update_timestamp();
`
2 changes: 2 additions & 0 deletions src/api/campaign.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export const schema = gql`
messageServiceLink: String
phoneNumbers: [String]
inventoryPhoneNumberCounts: [CampaignPhoneNumberCount]
useDynamicReplies: Boolean
replyBatchSize: Int
}
type CampaignsList {
Expand Down
7 changes: 7 additions & 0 deletions src/api/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const rootSchema = gql`
texterUIConfig: TexterUIConfigInput
timezone: String
inventoryPhoneNumberCounts: [CampaignPhoneNumberInput!]
useDynamicReplies: Boolean
replyBatchSize: Int
joinToken: String
}
input OrganizationInput {
Expand Down Expand Up @@ -395,6 +398,10 @@ const rootSchema = gql`
messageTextFilter: String
newTexterUserId: String!
): [CampaignIdAssignmentId]
dynamicReassign(
joinToken: String!
campaignId: String!
): String
importCampaignScript(campaignId: String!, url: String!): Int
createTag(organizationId: String!, tagData: TagInput!): Tag
editTag(organizationId: String!, id: String!, tagData: TagInput!): Tag
Expand Down
53 changes: 51 additions & 2 deletions src/components/CampaignDynamicAssignmentForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import GSTextField from "../components/forms/GSTextField";
import * as yup from "yup";
import Form from "react-formal";
import OrganizationJoinLink from "./OrganizationJoinLink";
import OrganizationReassignLink from "./OrganizationReassignLink";
import { dataTest } from "../lib/attributes";
import cloneDeep from "lodash/cloneDeep";
import TagChips from "./TagChips";
Expand Down Expand Up @@ -53,7 +54,7 @@ class CampaignDynamicAssignmentForm extends React.Component {

render() {
const { joinToken, campaignId, organization } = this.props;
const { useDynamicAssignment, batchPolicies } = this.state;
const { useDynamicAssignment, batchPolicies, useDynamicReplies } = this.state;
const unselectedPolicies = organization.batchPolicies
.filter(p => !batchPolicies.find(cur => cur === p))
.map(p => ({ id: p, name: p }));
Expand All @@ -73,6 +74,7 @@ class CampaignDynamicAssignmentForm extends React.Component {
label="Allow texters with a link to join and start texting when the campaign is started?"
labelPlacement="start"
/>
<br/>
<GSForm
schema={this.formSchema}
value={this.state}
Expand Down Expand Up @@ -133,6 +135,52 @@ class CampaignDynamicAssignmentForm extends React.Component {
message status filter in Message Review. You might set this to 48
hours for slower campaigns or 2 hours or less for GOTV campaigns.
</p>
<FormControlLabel
control={
<Switch
color="primary"
checked={useDynamicReplies || false}
onChange={(toggler, val) => {
console.log(toggler, val);
this.toggleChange("useDynamicReplies", val);
}}
/>
}
label="Allow texters with a link to dynamically get assigned replies?"
labelPlacement="start"
/>

{!useDynamicReplies ? null : (
<div>
<ul>
<li>
{joinToken ? (
<OrganizationReassignLink
joinToken={joinToken}
campaignId={campaignId}
/>
) : (
"Please save the campaign and reload the page to get the reply link to share with texters."
)}
</li>
<li>
You can turn off dynamic assignment after starting a campaign
to disallow more new texters to receive replies.
</li>
</ul>

<Form.Field
as={GSTextField}
fullWidth
name="replyBatchSize"
type="number"
label="How large should a batch of replies be?"
initialValue={200}
/>
</div>
)

}
{organization.batchPolicies.length > 1 ? (
<div>
<h3>Batch Strategy</h3>
Expand Down Expand Up @@ -211,7 +259,8 @@ CampaignDynamicAssignmentForm.propTypes = {
saveDisabled: type.bool,
joinToken: type.string,
responseWindow: type.number,
batchSize: type.string
batchSize: type.string,
replyBatchSize: type.string
};

export default compose(withMuiTheme)(CampaignDynamicAssignmentForm);
22 changes: 22 additions & 0 deletions src/components/OrganizationReassignLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import PropTypes from "prop-types";
import React from "react";
import DisplayLink from "./DisplayLink";

const OrganizationReassignLink = ({ joinToken, campaignId }) => {
let baseUrl = "https://base";
if (typeof window !== "undefined") {
baseUrl = window.location.origin;
}

const replyUrl = `${baseUrl}/${joinToken}/replies/${campaignId}`;
const textContent = `Send your texting volunteers this link! Once they sign up, they\'ll be automatically assigned replies for this campaign.`;

return <DisplayLink url={replyUrl} textContent={textContent} />;
};

OrganizationReassignLink.propTypes = {
joinToken: PropTypes.string,
campaignId: PropTypes.string
};

export default OrganizationReassignLink;
6 changes: 5 additions & 1 deletion src/containers/AdminCampaignEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ const campaignInfoFragment = `
state
count
}
useDynamicReplies
replyBatchSize
`;

export const campaignDataQuery = gql`query getCampaign($campaignId: String!) {
Expand Down Expand Up @@ -514,7 +516,9 @@ export class AdminCampaignEditBase extends React.Component {
"batchSize",
"useDynamicAssignment",
"responseWindow",
"batchPolicies"
"batchPolicies",
"useDynamicReplies",
"replyBatchSize"
],
checkCompleted: () => true,
blocksStarting: false,
Expand Down
2 changes: 2 additions & 0 deletions src/containers/AdminCampaignStats.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,11 @@ class AdminCampaignStats extends React.Component {
campaign.exportResults.campaignExportUrl.startsWith("http") ? (
<div>
Most recent export:
<br />
<a href={campaign.exportResults.campaignExportUrl} download>
Contacts Export CSV
</a>
<br />
<a
href={campaign.exportResults.campaignMessagesExportUrl}
download
Expand Down
Loading

0 comments on commit bcaf4d7

Please sign in to comment.