diff --git a/assets/js/api.js b/assets/js/api.js index 25994da3e..244fdd01d 100644 --- a/assets/js/api.js +++ b/assets/js/api.js @@ -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 () => { diff --git a/assets/js/components/channels/ChannelIndex.jsx b/assets/js/components/channels/ChannelIndex.jsx index 08a3aeb90..6a835c93b 100644 --- a/assets/js/components/channels/ChannelIndex.jsx +++ b/assets/js/components/channels/ChannelIndex.jsx @@ -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, + modalProvider: ?string, + modalIndex: ?number, +} + +class ChannelIndex extends Component { + state : State + + constructor(props) { + super(props) + this.state = { + modalLoading: false, + modalSurveys: [], + modalProvider: null, + modalIndex: null, + } + } -class ChannelIndex extends Component { componentDidMount() { this.props.actions.fetchChannels() } @@ -37,6 +57,23 @@ class ChannelIndex extends Component { 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) } @@ -88,6 +125,13 @@ class ChannelIndex extends Component { router, } = this.props + const { + modalLoading, + modalSurveys, + modalProvider, + modalIndex, + } = this.state + if (!channels) { return (
@@ -124,19 +168,18 @@ class ChannelIndex extends Component { } 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 ( - this.deleteProvider(provider, index)} - style={{ maxWidth: "600px" }} - showCancel + loading={loading} + surveys={surveys} /> ) } diff --git a/assets/js/components/channels/ProviderModal.jsx b/assets/js/components/channels/ProviderModal.jsx new file mode 100644 index 000000000..8887e1c96 --- /dev/null +++ b/assets/js/components/channels/ProviderModal.jsx @@ -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 ( + +
+

{t("Do you want to delete the channels provided by {{name}}?", { name })}

+ +
+ {loading ? {t("Searching active surveys...")} : + surveys.length == 0 ? {t("No active surveys")} : +
+ {t("These surveys are active, using channels from this provider. Deleting the channels will interrupt the surveys.")} +
    + {surveys.map((survey) => ( +
  • + {survey.name} +
  • + ))} +
+
} +
+
+
+ ) +} + +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) diff --git a/assets/vendor/css/materialize/components/_global.scss b/assets/vendor/css/materialize/components/_global.scss index 3f86a9294..86b54eb35 100755 --- a/assets/vendor/css/materialize/components/_global.scss +++ b/assets/vendor/css/materialize/components/_global.scss @@ -790,3 +790,14 @@ td, th{ cursor: default !important; } } + +.provider-surveys { + ul { + padding-left: 1rem; + list-style-type: disc; + + li { + list-style-type: disc; + } + } +} diff --git a/lib/ask/survey.ex b/lib/ask/survey.ex index bdf2101bf..3824b98be 100644 --- a/lib/ask/survey.ex +++ b/lib/ask/survey.ex @@ -20,7 +20,8 @@ defmodule Ask.Survey do RespondentStats, ConfigHelper, SystemTime, - PanelSurvey + PanelSurvey, + ProjectMembership } alias Ask.Ecto.Type.JSON @@ -530,6 +531,23 @@ 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, + 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() diff --git a/lib/ask_web/controllers/survey_controller.ex b/lib/ask_web/controllers/survey_controller.ex index 7d1a33813..1f812ad5b 100644 --- a/lib/ask_web/controllers/survey_controller.ex +++ b/lib/ask_web/controllers/survey_controller.ex @@ -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) diff --git a/lib/ask_web/router.ex b/lib/ask_web/router.ex index c41f3c49a..e266a660a 100644 --- a/lib/ask_web/router.ex +++ b/lib/ask_web/router.ex @@ -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 diff --git a/locales/template/translation.json b/locales/template/translation.json index 21ff7e9fe..bba418c69 100644 --- a/locales/template/translation.json +++ b/locales/template/translation.json @@ -288,6 +288,7 @@ "Named survey as {{newSurveyName}}": "", "New questionnaire": "", "Next step": "", + "No active surveys": "", "No cutoff": "", "No fallback": "", "No folder": "", @@ -417,6 +418,7 @@ "Sat": "", "Saving...": "", "Scheduled for {{dateString}}": "", + "Searching active surveys...": "", "Secondary color": "", "Section {{oldSectionTitle}} of {{questionnaireName}} renamed to {{newSectionTitle}}": "", "Select a channel...": "", @@ -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": "", diff --git a/test/ask/survey_test.exs b/test/ask/survey_test.exs index 88199bcec..36a61a30f 100644 --- a/test/ask/survey_test.exs +++ b/test/ask/survey_test.exs @@ -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) diff --git a/test/ask_web/controllers/survey_controller_test.exs b/test/ask_web/controllers/survey_controller_test.exs index 7ab5fd4af..0db2a948b 100644 --- a/test/ask_web/controllers/survey_controller_test.exs +++ b/test/ask_web/controllers/survey_controller_test.exs @@ -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)