-
Notifications
You must be signed in to change notification settings - Fork 39
Add anonymizing of user applications feature #4702
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 22 commits
debf643
838909b
f9ca7c1
93a7cca
f868859
d6ab39a
edaf68c
7f4c7fb
bc06495
5e49560
c7d6cb4
a70e3c1
ba60118
907e54c
9fd3b99
273cfca
a54deb8
dbc6fdf
a9b8b8f
7b7d9a0
b6ed347
549c4f8
3c9b438
9f25745
ca187ca
df2b0e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Anonymizing Submissions | ||
|
|
||
| Hypha offers submission anonymization to allow for less retention of Personally Identifiable Information (PII). | ||
|
|
||
| When the configuration option `SUBMISSION_ANONYMIZATION_ENABLED=True`, submissions can be either: | ||
|
|
||
| * Individually anonymized (Submission Detail View → "More Actions" → "Anonymize" button) | ||
| * Anonymized manually in bulk (Submissions All View → select submissions → "Anonymize" button in actions bar) | ||
| * Anonymized in bulk via user deletion (Wagtail Admin → "Settings" → "Users" → Select a user → "…" in top action/nav bar → "Delete" → "Anonymize all user submissions") | ||
| * Anonymized through the management command `python3 manage.py submission_cleanup --submissions [days]` (more info on that [here](cron-jobs.md#submission-cleanup)) | ||
|
|
||
| When a submission is anonymized, the original submission is deleted and a derivative is created with the following attributes: | ||
|
|
||
| * `value` - applied for value (if it exists on the original submission) | ||
| * `status` - status of the original submission | ||
| * `page` - either the fund/lab that the original submission existed on | ||
| * `round` - round (if any) the original submission was in | ||
| * `submit_time` - time the original submission was submitted | ||
| * `screening_status` - screening status of the original submission | ||
| * `category` - the categories of the original submission | ||
|
|
||
| and if the user that "owns" the submission still exists and isn't being actively deleted: | ||
|
|
||
| * `user` - stores the user that "owned" the original submission. When the user account is deleted, this field is set to null. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Generated by Django 5.2.13 on 2026-04-13 18:50 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("activity", "0091_alter_activity_options_and_more"), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name="activity", | ||
| name="user", | ||
| field=models.ForeignKey( | ||
| on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="event", | ||
| name="by", | ||
| field=models.ForeignKey( | ||
| null=True, | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| to=settings.AUTH_USER_MODEL, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="event", | ||
| name="type", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("UPDATE_LEAD", "updated lead"), | ||
| ("BATCH_UPDATE_LEAD", "batch updated lead"), | ||
| ("EDIT_SUBMISSION", "edited submission"), | ||
| ("APPLICANT_EDIT", "edited applicant"), | ||
| ("NEW_SUBMISSION", "submitted new submission"), | ||
| ("DRAFT_SUBMISSION", "submitted new draft submission"), | ||
| ("SCREENING", "screened"), | ||
| ("TRANSITION", "transitioned"), | ||
| ("BATCH_TRANSITION", "batch transitioned"), | ||
| ("DETERMINATION_OUTCOME", "sent determination outcome"), | ||
| ("BATCH_DETERMINATION_OUTCOME", "sent batch determination outcome"), | ||
| ("INVITED_TO_PROPOSAL", "invited to proposal"), | ||
| ("REVIEWERS_UPDATED", "updated reviewers"), | ||
| ("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"), | ||
| ("READY_FOR_REVIEW", "marked ready for review"), | ||
| ("BATCH_READY_FOR_REVIEW", "marked batch ready for review"), | ||
| ("NEW_REVIEW", "added new review"), | ||
| ("COMMENT", "added comment"), | ||
| ("PROPOSAL_SUBMITTED", "submitted proposal"), | ||
| ("OPENED_SEALED", "opened sealed submission"), | ||
| ("REVIEW_OPINION", "reviewed opinion"), | ||
| ("DELETE_SUBMISSION", "deleted submission"), | ||
| ("ANONYMIZE_SUBMISSION", "anonymized submission"), | ||
| ("DELETE_REVIEW", "deleted review"), | ||
| ("DELETE_REVIEW_OPINION", "deleted review opinion"), | ||
| ("CREATED_PROJECT", "created project"), | ||
| ("UPDATE_PROJECT_LEAD", "updated project lead"), | ||
| ("UPDATE_PROJECT_TITLE", "updated project title"), | ||
| ("EDIT_REVIEW", "edited review"), | ||
| ("SEND_FOR_APPROVAL", "sent for approval"), | ||
| ("APPROVE_PROJECT", "approved project"), | ||
| ("ASSIGN_PAF_APPROVER", "assign project form approver"), | ||
| ("APPROVE_PAF", "approved project form"), | ||
| ("PROJECT_TRANSITION", "transitioned project"), | ||
| ("REQUEST_PROJECT_CHANGE", "requested project change"), | ||
| ("SUBMIT_CONTRACT_DOCUMENTS", "submitted contract documents"), | ||
| ("UPLOAD_DOCUMENT", "uploaded document to project"), | ||
| ("UPLOAD_CONTRACT", "uploaded contract to project"), | ||
| ("APPROVE_CONTRACT", "approved contract"), | ||
| ("CREATE_INVOICE", "created invoice for project"), | ||
| ("UPDATE_INVOICE_STATUS", "updated invoice status"), | ||
| ("APPROVE_INVOICE", "approve invoice"), | ||
| ("DELETE_INVOICE", "deleted invoice"), | ||
| ("SENT_TO_COMPLIANCE", "sent project to compliance"), | ||
| ("UPDATE_INVOICE", "updated invoice"), | ||
| ("SUBMIT_REPORT", "submitted report"), | ||
| ("SKIPPED_REPORT", "skipped report"), | ||
| ("REPORT_FREQUENCY_CHANGED", "changed report frequency"), | ||
| ("DISABLED_REPORTING", "disabled reporting"), | ||
| ("REPORT_NOTIFY", "notified report"), | ||
| ("REVIEW_REMINDER", "reminder to review"), | ||
| ("BATCH_DELETE_SUBMISSION", "batch deleted submissions"), | ||
| ("BATCH_ANONYMIZE_SUBMISSION", "batch anonymized submissions"), | ||
| ("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"), | ||
| ("BATCH_INVOICE_STATUS_UPDATE", "batch update invoice status"), | ||
| ("STAFF_ACCOUNT_CREATED", "created new account"), | ||
| ("STAFF_ACCOUNT_EDITED", "edited account"), | ||
| ("ARCHIVE_SUBMISSION", "archived submission"), | ||
| ("UNARCHIVE_SUBMISSION", "unarchived submission"), | ||
| ("REMOVE_TASK", "remove task"), | ||
| ("INVITE_COAPPLICANT", "invite co-applicant"), | ||
| ], | ||
| max_length=50, | ||
| verbose_name="verb", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,7 +190,7 @@ def get_absolute_url(self): | |
| class Activity(models.Model): | ||
| timestamp = models.DateTimeField() | ||
| type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30) | ||
| user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) | ||
| user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) | ||
|
Member
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. This needs to be documented I think. Deleting a user will now delete the audit trail. Would it not be better to anonymise the user account instead of deleting it? Or do the activities contain sensitive info?
Contributor
Author
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. I'm thinking that would likely be another PR/feature if we wanted to go that route. As it stands I'm the only one at OTF that can delete a user when a GDPR request comes in for deletion because all staff member gets in wagtail is an error saying the user is used elsewhere but never pointed to which specific activity that is. This is definitely more of a nuclear option but definitely GDPR compliant as activities can contain a host of data especially when it's a comment - open to your thoughts on this though! |
||
|
|
||
| source_content_type = models.ForeignKey( | ||
| ContentType, | ||
|
|
@@ -328,7 +328,7 @@ class Event(models.Model): | |
| when = models.DateTimeField(auto_now_add=True) | ||
| type = models.CharField(_("verb"), choices=MESSAGES.choices, max_length=50) | ||
| by = models.ForeignKey( | ||
| settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True | ||
| settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True | ||
| ) | ||
| content_type = models.ForeignKey( | ||
| ContentType, blank=True, null=True, on_delete=models.CASCADE | ||
|
|
||
This file was deleted.
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.
Are categories always preserved? Will try to test this for bulk_anonymize_submissions, submission_cleanup and on user delete.
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.
yes they should be as of the latest push, needed to add
form_fieldsto thevalues(...)call for them to be parsed