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

✨ Allow users to redact description #714

Merged
merged 2 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion froide/foirequest/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ class FoiRequestListSerializer(serializers.HyperlinkedModelSerializer):
read_only=True, view_name="api:campaign-detail", lookup_field="pk"
)
tags = TagListField()
description = serializers.CharField(source="get_description")

redacted_description = serializers.SerializerMethodField()

class Meta:
model = FoiRequest
Expand All @@ -307,6 +308,7 @@ class Meta:
"public",
"law",
"description",
"redacted_description",
"summary",
"same_as_count",
"same_as",
Expand Down Expand Up @@ -336,6 +338,13 @@ def get_user(self, obj):
return None
return obj.user.pk

def get_redacted_description(self, obj):
request = self.context["request"]
authenticated_read = can_read_foirequest_authenticated(
obj, request, allow_code=False
)
return obj.get_redacted_description(authenticated_read)


class FoiRequestDetailSerializer(FoiRequestListSerializer):
public_body = PublicBodySerializer(read_only=True)
Expand Down
30 changes: 3 additions & 27 deletions froide/foirequest/forms/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
from froide.account.services import AccountService
from froide.helper.fields import MultipleFileField
from froide.helper.storage import make_filename, make_unique_filename
from froide.helper.text_diff import get_diff_chunks
from froide.helper.text_utils import redact_subject
from froide.helper.text_utils import apply_user_redaction, redact_subject
from froide.helper.widgets import (
BootstrapCheckboxInput,
BootstrapFileInput,
Expand Down Expand Up @@ -809,32 +808,9 @@ def clean(self):
if not self.cleaned_data.get("subject_length"):
raise forms.ValidationError

def redact_part(self, original, instructions, length):
REDACTION_MARKER = str(_("[redacted]"))

if not instructions:
return original

chunks = get_diff_chunks(original)

# Sanity check chunk length
if len(chunks) != length:
raise IndexError

for index in instructions:
chunks[index] = REDACTION_MARKER

redacted = "".join(chunks)
# Replace multiple connecting redactions with one
return re.sub(
"{marker}(?: {marker})+".format(marker=re.escape(REDACTION_MARKER)),
REDACTION_MARKER,
redacted,
)

def save(self, message):
try:
redacted_subject = self.redact_part(
redacted_subject = apply_user_redaction(
message.subject,
self.cleaned_data["subject"],
self.cleaned_data["subject_length"],
Expand All @@ -844,7 +820,7 @@ def save(self, message):
logging.warning(e)

try:
redacted_content = self.redact_part(
redacted_content = apply_user_redaction(
message.plaintext,
self.cleaned_data["content"],
self.cleaned_data["content_length"],
Expand Down
44 changes: 43 additions & 1 deletion froide/foirequest/forms/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from django import forms
from django.conf import settings
from django.contrib import messages
from django.http import HttpRequest
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.html import escape
Expand All @@ -16,7 +18,7 @@
from froide.helper.auth import get_read_queryset
from froide.helper.form_utils import JSONMixin
from froide.helper.forms import TagObjectForm
from froide.helper.text_utils import redact_plaintext, slugify
from froide.helper.text_utils import apply_user_redaction, redact_plaintext, slugify
from froide.helper.widgets import BootstrapRadioSelect, BootstrapSelect, PriceInput
from froide.publicbody.models import PublicBody
from froide.publicbody.widgets import PublicBodySelect
Expand Down Expand Up @@ -449,3 +451,43 @@ def save(self) -> List[str]:
m for m in trigger.apply_actions(self.foirequest, self.request) if m
]
return messages


class RedactDescriptionForm(forms.Form):
description = forms.CharField(required=False)
description_length = forms.IntegerField()

def clean_description(self):
val = self.cleaned_data["description"]
if not val:
return []
try:
val = [int(x) for x in val.split(",")]
except ValueError:
raise forms.ValidationError("Bad value")
return val

def save(self, request: HttpRequest, foirequest: FoiRequest):
redacted_description = apply_user_redaction(
foirequest.description,
self.cleaned_data["description"],
self.cleaned_data["description_length"],
)
first_message = foirequest.first_outgoing_message
if foirequest.description_redacted in first_message.plaintext_redacted:
first_message.plaintext_redacted = first_message.plaintext_redacted.replace(
foirequest.description_redacted, redacted_description
)
first_message.save()
else:
messages.add_message(
request,
pajowu marked this conversation as resolved.
Show resolved Hide resolved
messages.WARNING,
_(
"Could not automatically redact first message. Please check the message manually."
),
)

foirequest.description_redacted = redacted_description

foirequest.save()
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.4 on 2023-10-05 16:43

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("foirequest", "0064_foimessage_confirmation_sent"),
]

operations = [
migrations.AddField(
model_name="foirequest",
name="description_redacted",
field=models.TextField(blank=True, verbose_name="Redacted Description"),
),
]
2 changes: 2 additions & 0 deletions froide/foirequest/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class EventName(models.TextChoices):
PUBLIC_BODY_SUGGESTED = "public_body_suggested", _("a public body was suggested")
REQUEST_REDIRECTED = "request_redirected", _("the request was redirected")

DESCRIPTION_REDACTED = "description_redacted", _("the description was redacted")


EVENT_KEYS = dict(EventName.choices).keys()

Expand Down
27 changes: 24 additions & 3 deletions froide/foirequest/models/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from collections import namedtuple
from datetime import timedelta
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, List, Optional, Tuple

import django.dispatch
from django.conf import settings
Expand All @@ -22,6 +22,7 @@

from froide.campaign.models import Campaign
from froide.helper.email_utils import make_address
from froide.helper.text_diff import get_differences
from froide.helper.text_utils import redact_plaintext
from froide.publicbody.models import FoiLaw, Jurisdiction, PublicBody
from froide.team.models import Team
Expand Down Expand Up @@ -295,6 +296,7 @@ class FoiRequest(models.Model):
title = models.CharField(_("Title"), max_length=255)
slug = models.SlugField(_("Slug"), max_length=255, unique=True)
description = models.TextField(_("Description"), blank=True)
description_redacted = models.TextField(_("Redacted Description"), blank=True)
summary = models.TextField(_("Summary"), blank=True)

public_body = models.ForeignKey(
Expand Down Expand Up @@ -627,8 +629,27 @@ def get_redaction_regexes(self):
return json.dumps([a.strip() for a in all_regexes if a.strip()])

def get_description(self):
user_replacements = self.user.get_redactions()
return redact_plaintext(self.description, user_replacements=user_replacements)
if not self.description_redacted:
user_replacements = self.user.get_redactions()
self.description_redacted = redact_plaintext(
self.description, user_replacements=user_replacements
)
self.save(update_fields=["description_redacted"])
return self.description_redacted

def get_redacted_description(self, auth: bool) -> List[Tuple[bool, str]]:
if auth:
show, hide = (
self.description,
self.get_description(),
)
else:
show, hide = (
self.get_description(),
self.description,
)

return [x for x in get_differences(show, hide)]

def response_messages(self):
return list(filter(lambda m: m.is_response, self.messages))
Expand Down
7 changes: 5 additions & 2 deletions froide/foirequest/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from froide.helper.db_utils import save_obj_with_slug
from froide.helper.email_parsing import ParsedEmail
from froide.helper.storage import make_unique_filename
from froide.helper.text_utils import redact_subject
from froide.helper.text_utils import redact_plaintext, redact_subject
from froide.problem.models import ProblemReport

from .hooks import registry
Expand Down Expand Up @@ -166,13 +166,17 @@ def create_project(self):
def create_request(self, publicbody, sequence=0):
data = self.data
user = data["user"]
user_replacements = user.get_redactions()

now = timezone.now()
request = FoiRequest(
title=data["subject"],
public_body=publicbody,
user=data["user"],
description=data["body"],
description_redacted=redact_plaintext(
data["body"], user_replacements=user_replacements
),
public=data["public"],
language=data.get("language", ""),
site=Site.objects.get_current(),
Expand Down Expand Up @@ -219,7 +223,6 @@ def create_request(self, publicbody, sequence=0):
request.tags.add(*[t[:100] for t in data["tags"]])

subject = "%s [#%s]" % (request.title, request.pk)
user_replacements = user.get_redactions()
message = FoiMessage(
request=request,
sent=False,
Expand Down
Loading
Loading