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

Add active surveys to the warning message when deleting a channel #2374

Merged
merged 9 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
6 changes: 6 additions & 0 deletions assets/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ export const newWave = (projectId, panelSurveyId) => {
return apiPostJSON(`projects/${projectId}/panel_surveys/${panelSurveyId}/new_wave`, surveySchema)
}

export const fetchActiveSurveys = (provider, baseUrl) => {
return apiFetchJSON(
`surveys/active_channels/${provider}?base_url=${encodeURIComponent(baseUrl)}`,
)
}

export const fetchTimezones = () => {
return apiFetchJSONWithCallback(`timezones`, null, {}, (json, schema) => {
return () => {
Expand Down
67 changes: 55 additions & 12 deletions assets/js/components/channels/ChannelIndex.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,35 @@ import {
UntitledIfEmpty,
SortableHeader,
Modal,
ConfirmationModal,
PagingFooter,
channelFriendlyName,
} from "../ui"
import { Preloader } from "react-materialize"
import { config } from "../../config"
import { translate } from "react-i18next"
import ProviderModal from "./ProviderModal"
import * as api from "../../api"

type State = {
modalLoading: boolean,
modalSurveys: Array<Object>,
modalProvider: ?string,
modalIndex: ?number,
}

class ChannelIndex extends Component<any, State> {
state : State

constructor(props) {
super(props)
this.state = {
modalLoading: false,
modalSurveys: [],
modalProvider: null,
modalIndex: null,
}
}

class ChannelIndex extends Component<any> {
componentDidMount() {
this.props.actions.fetchChannels()
}
Expand All @@ -37,6 +57,23 @@ class ChannelIndex extends Component<any> {
toggleProvider(provider, index, checked) {
if (checked) {
$(`#${provider}Modal-${index}`).modal("open")
this.setState({
modalLoading: true,
modalSurveys: [],
modalProvider: provider,
modalIndex: index,
})
const { baseUrl } = config[provider][index]
api.fetchActiveSurveys(provider, baseUrl)
.then((response) => {
const surveys = response || []
this.setState({
modalLoading: false,
modalSurveys: surveys,
modalProvider: provider,
modalIndex: index,
})
})
} else {
this.props.authActions.toggleAuthorization(provider, index)
}
Expand Down Expand Up @@ -88,6 +125,13 @@ class ChannelIndex extends Component<any> {
router,
} = this.props

const {
modalLoading,
modalSurveys,
modalProvider,
modalIndex,
} = this.state

if (!channels) {
return (
<div>
Expand Down Expand Up @@ -124,19 +168,18 @@ class ChannelIndex extends Component<any> {
}

const providerModal = (provider, index, friendlyName, multiple) => {
let name = `${provider[0].toUpperCase()}${provider.slice(1)}`
if (multiple) name = `${name} (${friendlyName})`

const loading = provider === modalProvider && index === modalIndex ? modalLoading : false
const surveys = provider === modalProvider && index === modalIndex ? modalSurveys : []
return (
<ConfirmationModal
<ProviderModal
key={`${provider}-${index}`}
modalId={`${provider}Modal-${index}`}
modalText={t("Do you want to delete the channels provided by {{name}}?", { name })}
header={t("Turn off {{name}}", { name })}
confirmationText={t("Yes")}
provider={provider}
index={index}
friendlyName={friendlyName}
multiple={multiple}
onConfirm={() => this.deleteProvider(provider, index)}
style={{ maxWidth: "600px" }}
showCancel
loading={loading}
surveys={surveys}
/>
)
}
Expand Down
60 changes: 60 additions & 0 deletions assets/js/components/channels/ProviderModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { PropTypes } from "react"
import { translate } from "react-i18next"
import { ConfirmationModal } from "../ui"

export const ProviderModal = ({
t,
provider,
index,
friendlyName,
multiple,
onConfirm,
loading,
surveys,
}) => {
let name = `${provider[0].toUpperCase()}${provider.slice(1)}`
if (multiple) name = `${name} (${friendlyName})`

return (
<ConfirmationModal
modalId={`${provider}Modal-${index}`}
header={t("Turn off {{name}}", { name })}
confirmationText={t("Yes")}
onConfirm={onConfirm}
style={{ maxWidth: "600px" }}
showCancel
>
<div>
<p>{t("Do you want to delete the channels provided by {{name}}?", { name })}</p>

<div className="provider-surveys">
{loading ? <span>{t("Searching active surveys...")}</span> :
surveys.length == 0 ? <span>{t("No active surveys")}</span> :
<div>
<span>{t("These surveys are active, using channels from this provider. Deleting the channels will interrupt the surveys.")}</span>
<ul>
{surveys.map((survey) => (
<li key={`survey-${survey.id}`}>
<span>{survey.name}</span>
</li>
))}
</ul>
</div>}
</div>
</div>
</ConfirmationModal>
)
}

ProviderModal.propTypes = {
t: PropTypes.func,
provider: PropTypes.string,
index: PropTypes.number,
friendlyName: PropTypes.string,
multiple: PropTypes.bool,
onConfirm: PropTypes.func,
loading: PropTypes.bool,
surveys: PropTypes.any,
}

export default translate()(ProviderModal)
11 changes: 11 additions & 0 deletions assets/vendor/css/materialize/components/_global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -790,3 +790,14 @@ td, th{
cursor: default !important;
}
}

.provider-surveys {
ul {
padding-left: 1rem;
list-style-type: disc;

li {
list-style-type: disc;
}
}
}
24 changes: 23 additions & 1 deletion lib/ask/survey.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ defmodule Ask.Survey do
RespondentStats,
ConfigHelper,
SystemTime,
PanelSurvey
PanelSurvey,
ProjectMembership
}

alias Ask.Ecto.Type.JSON
Expand Down Expand Up @@ -530,6 +531,27 @@ defmodule Ask.Survey do
%{survey | down_channels: down_channels}
end

def with_active_channels(user_id, provider, base_url) do
query =
from s in Survey,
where: s.state == :running,
join: pm in ProjectMembership,
on: pm.project_id == s.project_id and pm.user_id == ^user_id,
select: s

query =
from s in subquery(query),
ismaelbej marked this conversation as resolved.
Show resolved Hide resolved
join: group in RespondentGroup,
on: s.id == group.survey_id,
join: rgc in RespondentGroupChannel,
on: group.id == rgc.respondent_group_id,
join: c in Channel,
on: rgc.channel_id == c.id and c.provider == ^provider and c.base_url == ^base_url,
select: s

query |> Repo.all()
end

def stats(survey) do
respondents_by_disposition = survey |> RespondentStats.respondents_by_disposition()

Expand Down
6 changes: 6 additions & 0 deletions lib/ask_web/controllers/survey_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,12 @@ defmodule AskWeb.SurveyController do
end
end

def active_channels(conn, %{"provider" => provider, "base_url" => base_url}) do
surveys = Survey.with_active_channels(current_user(conn).id, provider, base_url)

render(conn, "index.json", surveys: surveys)
end

defp load_survey(project, survey_id) do
project
|> assoc(:surveys)
Expand Down
2 changes: 2 additions & 0 deletions lib/ask_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ defmodule AskWeb.Router do
get "/get_invite_by_email_and_project", InviteController, :get_by_email_and_project
get "/settings", UserController, :settings, as: :settings
post "/update_settings", UserController, :update_settings, as: :update_settings

get "/surveys/active_channels/:provider", SurveyController, :active_channels, as: :surveys_active_channels
end
end

Expand Down
3 changes: 3 additions & 0 deletions locales/template/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"Named survey as <i>{{newSurveyName}}</i>": "",
"New questionnaire": "",
"Next step": "",
"No active surveys": "",
"No cutoff": "",
"No fallback": "",
"No folder": "",
Expand Down Expand Up @@ -417,6 +418,7 @@
"Sat": "",
"Saving...": "",
"Scheduled for {{dateString}}": "",
"Searching active surveys...": "",
"Secondary color": "",
"Section <i>{{oldSectionTitle}}</i> of <i>{{questionnaireName}}</i> renamed to <i>{{newSectionTitle}}</i>": "",
"Select a channel...": "",
Expand Down Expand Up @@ -475,6 +477,7 @@
"The schedule of your survey restricts the days and hours during which respondents will be contacted. You can also specify re-contact attempts intervals.": "",
"The selected questionnaire will be sent over the survey channels to every respondent until a cutoff rule is reached. If you wish, you can try an experiment to compare questionnaires performance.": "",
"The system only accepts CSV files": "",
"These surveys are active, using channels from this provider. Deleting the channels will interrupt the surveys.": "",
"This is a wave of a panel survey. The settings from previous waves will be used as a template for this wave. Any changes made to this wave's settings will serve as a template for future waves of this panel survey": "",
"This question is not relevant for partial flag": "",
"This question is relevant for partial flag": "",
Expand Down
38 changes: 38 additions & 0 deletions test/ask/survey_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,44 @@ defmodule Ask.SurveyTest do
assert running_channels == [Enum.at(channels, 1).id]
end

test "enumerates surveys with active channel" do
user = insert(:user)
project = create_project_for_user(user)

user2 = insert(:user)
project2 = create_project_for_user(user2)

surveys = [
insert(:survey, state: :ready, project: project),
insert(:survey, state: :running, project: project),
insert(:survey, state: :running, project: project2),
insert(:survey, state: :running, project: project2),
]

channels = [
insert(:channel, provider: "sms", base_url: "test", projects: [project]),
insert(:channel, provider: "sms", base_url: "test", projects: [project]),
insert(:channel, provider: "ivr", base_url: "prod", projects: [project2]),
insert(:channel, provider: "sms", base_url: "test", projects: [project2]),
]

setup_surveys_with_channels(surveys, channels)

active_surveys =
Survey.with_active_channels(user.id, "sms", "test")
|> Enum.map(fn c -> c.id end)
|> Enum.sort()

assert active_surveys == [Enum.at(surveys, 1).id]

active_surveys2 =
Survey.with_active_channels(user2.id, "sms", "test")
|> Enum.map(fn c -> c.id end)
|> Enum.sort()

assert active_surveys2 == [Enum.at(surveys, 3).id]
end

test "enumerates channels of a survey" do
survey = insert(:survey)
channel_1 = insert(:channel)
Expand Down
60 changes: 60 additions & 0 deletions test/ask_web/controllers/survey_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3282,6 +3282,66 @@ defmodule AskWeb.SurveyControllerTest do
end
end

test "surveys with active channel", %{conn: conn, user: user} do
project = create_project_for_user(user)
surveys = [
insert(:survey, project: project, state: :not_ready),
insert(:survey, project: project, state: :running),
]
channels = [
insert(:channel, provider: "sms", base_url: "test"),
insert(:channel, provider: "sms", base_url: "test"),
]
setup_surveys_with_channels(surveys, channels)
survey = Survey |> Repo.get(Enum.at(surveys, 1).id)

result = get(conn, surveys_active_channels_path(conn, :active_channels, "sms", base_url: "test"))

assert json_response(result, 200)["data"] == [
%{
"cutoff" => survey.cutoff,
"id" => survey.id,
"mode" => survey.mode,
"name" => survey.name,
"description" => nil,
"project_id" => project.id,
"state" => "running",
"locked" => false,
"exit_code" => nil,
"exit_message" => nil,
"schedule" => %{
"blocked_days" => [],
"day_of_week" => %{
"fri" => true,
"mon" => true,
"sat" => true,
"sun" => true,
"thu" => true,
"tue" => true,
"wed" => true
},
"end_time" => "23:59:59",
"start_time" => "00:00:00",
"start_date" => nil,
"end_date" => nil,
"timezone" => "Etc/UTC"
},
"next_schedule_time" => nil,
"started_at" => nil,
"ended_at" => nil,
"updated_at" => to_iso8601(survey.updated_at),
"down_channels" => [],
"folder_id" => nil,
"first_window_started_at" => nil,
"panel_survey_id" => nil,
"last_window_ends_at" => nil,
"is_deletable" => false,
"is_movable" => true,
"generates_panel_survey" => false
},
]
end

def prepare_for_state_update(user) do
project = create_project_for_user(user)
questionnaire = insert(:questionnaire, name: "test", project: project)
Expand Down