Skip to content

Conversation

@matiasgarciaisaia
Copy link
Member

@matiasgarciaisaia matiasgarciaisaia commented Aug 13, 2025

Let users import into a survey all the uncontacted respondents from another finished one.

The source survey must be already terminated, and has to belong to the same project as the target one.

surveda-import-sample.webm

Error handling:
image

Fixes #2389

@matiasgarciaisaia matiasgarciaisaia force-pushed the feature/2389-import-unused-sample branch from 645c2b7 to 46cdcfe Compare August 13, 2025 17:21
@matiasgarciaisaia matiasgarciaisaia force-pushed the feature/2389-import-unused-sample branch from 46cdcfe to 64a54c5 Compare August 28, 2025 23:31
@matiasgarciaisaia matiasgarciaisaia marked this pull request as ready for review August 28, 2025 23:37
@matiasgarciaisaia matiasgarciaisaia force-pushed the feature/2389-import-unused-sample branch 2 times, most recently from fa1e4fb to ba4bfcb Compare August 29, 2025 23:13
We still have to pick the right survey, and probably show a preview.

See #2389
We still have to implement the source survey picker, and check it's
finished.

See #2389
We still have to show the unused respondents count, and maybe fix the
style a bit.

There's also some error handling left before finishing the feature.

See #2389
See #2390
We don't want to import from surveys that are still running.

See #2389
We now have to use it from the UI.

See #2389
The state changed in both 28fa1a3 and f1160f2
It was copied from another modal, but then turned up unnecessary.

See #2389
@matiasgarciaisaia matiasgarciaisaia force-pushed the feature/2389-import-unused-sample branch from ba4bfcb to 34a4e79 Compare August 29, 2025 23:23
Copy link
Member

@ggiraldez ggiraldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good as far as I can evaluate with minimal Elixir and completely rusted redux/react knowledge. Left a small suggestion on some query, but it's probably not too important.

Comment on lines 70 to 71
where: s.project_id == ^project.id and s.state == :terminated,
select: %{survey_id: s.id, name: s.name, ended_at: s.ended_at, respondents: sum(fragment("if(?, ?, ?)", r.disposition == :registered, 1, 0))},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this could also be a select ..., count(*) as respondents from ... where r.disposition = 'registered' which may be more efficient. I'm not sure about the Ecto syntax though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Ecto syntax is not a problem:

        from s in Survey,
          left_join: r in Respondent,
          on: r.survey_id == s.id,
          where: s.project_id == ^project.id and s.state == :terminated and r.disposition == :registered,
          select: %{survey_id: s.id, name: s.name, ended_at: s.ended_at, respondents: count(s.id)},
          group_by: [s.id],
          order_by: [desc: count(s.id)]

But then you don't get the surveys that have 0 available respondents listed. I'd rather explicitely show that there are no available respondents rather than ignore the survey at all.

I could, though, add a comment on the code explaining this.

There's probably another query that can be written for that, but not sure it'd be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. You can use a left join for that, and count(r.id) or any other field that is NULL when joining an empty set.

Copy link
Member Author

@matiasgarciaisaia matiasgarciaisaia Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhhh! The key is to add the r.disposition = 'terminated' condition to the JOIN instead of the WHERE (third query in the screenshot), so the join is made with an empty half-row. When the condition is on the WHERE (second query), you first match surveys and respondents, but then discard the joined row altogether for not satisfying the condition.

Image

sample_name = "__imported_from_survey_#{source_survey.id}.csv"
case RespondentGroupAction.load_entries(entries, survey) do
{:ok, loaded_entries} ->
survey |> RespondentGroupAction.disable_incentives_if_disabled_in_source!(source_survey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may merit a comment. Why is adding a respondent group to the survey updating an attribute of the survey?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a business rule - if we're importing contacts from a deanonimized survey, this one is now also deanonimized.

I guess we can't compute that value from the respondent groups "at runtime" in case one can "game" the system by adding/removing respondent groups somehow - but I'm not really sure about that. I mostly followed what we're doing when adding respondents from CSVs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I guess what threw me off was mostly that the updating function is in the respondent group file, but it updates the survey. I see that the module is RespondentGroupAction, but I still find it a bit confusing. Also, how are incentives related to anonymization? Is that legacy naming?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not legacy. Incentives distribution is made by having the user download a list of phone numbers, and that's the only way for a user to get phone numbers out of Surveda. Those numbers already responded surveys, so there's potential to link responses to phone numbers - breaking the anonymity.

There might be a better way to model this "tainting" of the respondent group, but I'm not sure which one that'd be.

@matiasgarciaisaia matiasgarciaisaia merged commit c28a70e into main Sep 1, 2025
2 checks passed
matiasgarciaisaia added a commit that referenced this pull request Sep 4, 2025
After what we discussed with @ggiraldez in #2391
matiasgarciaisaia added a commit that referenced this pull request Sep 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import unused sample from a finished survey

3 participants