Repository navigation
Add SurveyPurpose.FOLLOWUP for Program V2 forms #1082
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,14 +65,17 @@ def mutate( | |
| if survey.login_required and not revision_created_by: | ||
| raise Exception("Login required") | ||
|
|
||
| if not survey.workflow.can_be_responded_by(request): | ||
| raise Exception("You are not allowed to respond to this survey") | ||
|
|
||
| if survey.max_responses_per_user: # noqa: SIM102 | ||
| if ( | ||
| survey.current_responses.filter(revision_created_by=revision_created_by).count() | ||
| >= survey.max_responses_per_user | ||
| ): | ||
| raise Exception("Maximum number of responses reached") | ||
|
|
||
| if survey.purpose != SurveyPurpose.DEFAULT and old_version is None: | ||
| if survey.purpose not in (SurveyPurpose.DEFAULT, SurveyPurpose.FOLLOWUP) and old_version is None: | ||
| raise Exception("Special purpose surveys cannot be submitted via this endpoint") | ||
|
|
||
| if survey.anonymity == "HARD": | ||
|
Comment on lines
65
to
81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- mutation ---'
sed -n '1,115p' kompassi/forms/graphql/mutations/create_survey_response.py
printf '%s\n' '--- editability helpers ---'
sed -n '450,545p' kompassi/forms/models/workflow.py
printf '%s\n' '--- current diff ---'
git diff --unified=35 6765b1477a4ae3841a91bf816bc0860226c8dd82 e6e54b52b7c5f28c101a8aa4452a6cb950562684 -- kompassi/forms/graphql/mutations/create_survey_response.pyRepository: con2/kompassi Length of output: 12653 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- FOLLOWUP workflow ---'
sed -n '1,180p' kompassi/program_v2/workflows/program_followup.py
printf '%s\n' '--- workflow editability overrides ---'
rg -n -C 8 'def (response_can_be_edited_by|response_can_be_edited_by_owner|response_can_be_edited_by_admin|can_be_responded_by)' kompassi
printf '%s\n' '--- comparable mutation authorization ---'
rg -n -C 12 'response_can_be_edited_by|can_be_responded_by' kompassi/forms kompassi/program_v2 --glob '*.py'Repository: con2/kompassi Length of output: 31216 Recheck FOLLOWUP eligibility for owner edits.
Add the eligibility check to the FOLLOWUP owner-edit path. Keep the admin path unchanged. Suggested fix class ProgramFollowupWorkflow(ProgramHostInvitationWorkflow):
@@
def can_be_responded_by(self, request: HttpRequest) -> bool:
user = request.user
if not user.is_authenticated:
return False
@@
return self._program_host_involvements(person).exists()
+ def response_can_be_edited_by_owner(self, response: Response, request: HttpRequest) -> bool:
+ return super().response_can_be_edited_by_owner(response, request) and self.can_be_responded_by(request)
+
def _program_host_involvements(self, person):🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # Generated by Django 6.1.1 on 2026-10-02 13:45 | ||
|
|
||
| import django.db.models.deletion | ||
| import django_enum.fields | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ # noqa: RUF012 | ||
| ("core", "0045_person_program_role_retention_policy"), | ||
| ("dimensions", "0020_propagate_annotation_form_fields_to_forms"), | ||
| ("forms", "0058_survey_retention_period"), | ||
| ("involvement", "0014_alter_involvementeventmeta_default_registry"), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ # noqa: RUF012 | ||
| migrations.RemoveConstraint( | ||
| model_name="survey", | ||
| name="forms_Survey_purpose_SurveyPurpose", | ||
| ), | ||
| migrations.AddField( | ||
| model_name="response", | ||
| name="parent", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="Reserved: the response a follow-up response follows up on. Must always point to the newest version of the parent response.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| related_name="followups", | ||
| to="forms.response", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="survey", | ||
| name="parent", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="Reserved for follow-up surveys that follow up on the responses of a parent survey.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.PROTECT, | ||
| related_name="followups", | ||
| to="forms.survey", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="survey", | ||
| name="purpose", | ||
| field=django_enum.fields.EnumCharField( | ||
| choices=[("DEFAULT", "DEFAULT"), ("INVITE", "INVITE"), ("FOLLOWUP", "FOLLOWUP")], | ||
| default="DEFAULT", | ||
| help_text="Generic surveys and program offers are DEFAULT, program host invitations are INVITE, follow-up questions to existing program hosts are FOLLOWUP.", | ||
| max_length=8, | ||
| ), | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name="survey", | ||
| constraint=models.CheckConstraint( | ||
| condition=models.Q(("purpose__in", ["DEFAULT", "INVITE", "FOLLOWUP"])), | ||
| name="forms_Survey_purpose_SurveyPurpose", | ||
| ), | ||
| ), | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: con2/kompassi
Length of output: 1526
🏁 Script executed:
Repository: con2/kompassi
Length of output: 41578
🏁 Script executed:
Repository: con2/kompassi
Length of output: 41290
🏁 Script executed:
Repository: con2/kompassi
Length of output: 38821
🏁 Script executed:
Repository: con2/kompassi
Length of output: 38500
🏁 Script executed:
Repository: con2/kompassi
Length of output: 27286
🏁 Script executed:
Repository: con2/kompassi
Length of output: 14046
Do not treat an active
PROGRAM_HOSTinvolvement as proof of transfer consent.ProgramFollowupWorkflowgrants access to any authenticated user with an activePROGRAM_HOSTinvolvement.AcceptInvitationaccepts arbitraryform_dataand creates that involvement without checkingkompassiTransferConsent. The checkbox exists only in the client-sideTransferConsentForm.A request can therefore create an eligible host without explicit transfer consent. The follow-up page then hides the consent form, and the follow-up workflow writes submitted values to that host’s involvement. Enforce consent server-side before creating the involvement and before allowing follow-up responses, or retain the consent form until a persisted consent record exists.
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. This is too big to handle as part of this PR; recorded separately as #1083.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@japsuUnderstood. The consent enforcement change is deferred to#1083and remains outside this PR’s scope.You are interacting with an AI system.