diff --git a/froide/foirequest/api_views.py b/froide/foirequest/api_views.py index a88554a3d..0fa47b7dd 100644 --- a/froide/foirequest/api_views.py +++ b/froide/foirequest/api_views.py @@ -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 @@ -307,6 +308,7 @@ class Meta: "public", "law", "description", + "redacted_description", "summary", "same_as_count", "same_as", @@ -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) diff --git a/froide/foirequest/forms/message.py b/froide/foirequest/forms/message.py index 0db2a7b3f..c765ef980 100644 --- a/froide/foirequest/forms/message.py +++ b/froide/foirequest/forms/message.py @@ -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, @@ -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"], @@ -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"], diff --git a/froide/foirequest/forms/request.py b/froide/foirequest/forms/request.py index 69351d4d9..e7f6a976a 100644 --- a/froide/foirequest/forms/request.py +++ b/froide/foirequest/forms/request.py @@ -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 @@ -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 @@ -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, + messages.WARNING, + _( + "Could not automatically redact first message. Please check the message manually." + ), + ) + + foirequest.description_redacted = redacted_description + + foirequest.save() diff --git a/froide/foirequest/migrations/0065_foirequest_description_redacted.py b/froide/foirequest/migrations/0065_foirequest_description_redacted.py new file mode 100644 index 000000000..7189da69f --- /dev/null +++ b/froide/foirequest/migrations/0065_foirequest_description_redacted.py @@ -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"), + ), + ] diff --git a/froide/foirequest/models/event.py b/froide/foirequest/models/event.py index f91bc1f75..428cd241d 100644 --- a/froide/foirequest/models/event.py +++ b/froide/foirequest/models/event.py @@ -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() diff --git a/froide/foirequest/models/request.py b/froide/foirequest/models/request.py index 93194d1ef..bb03c9503 100644 --- a/froide/foirequest/models/request.py +++ b/froide/foirequest/models/request.py @@ -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 @@ -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 @@ -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( @@ -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)) diff --git a/froide/foirequest/services.py b/froide/foirequest/services.py index 1f8fa4e72..27d905c26 100644 --- a/froide/foirequest/services.py +++ b/froide/foirequest/services.py @@ -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 @@ -166,6 +166,7 @@ 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( @@ -173,6 +174,9 @@ def create_request(self, publicbody, sequence=0): 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(), @@ -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, diff --git a/froide/foirequest/templates/foirequest/foiproject_detail.html b/froide/foirequest/templates/foirequest/foiproject_detail.html index cf0aacede..42f9b98f9 100644 --- a/froide/foirequest/templates/foirequest/foiproject_detail.html +++ b/froide/foirequest/templates/foirequest/foiproject_detail.html @@ -5,161 +5,160 @@ {% load foirequest_tags %} {% load form_helper %} {% block title %}{{ object.title }}{% endblock %} -{% block metadescription %}{{ object.get_description }}{% endblock %} {% block extra_head %} - {% if not object.private %}{% endif %} - - - {% if request.user.is_staff %} - {% with object.get_set_tags_form as set_tags_form %}{{ set_tags_form.media.css }}{% endwith %} - {% endif %} + {% if not object.private %}{% endif %} + + + {% if request.user.is_staff %} + {% with object.get_set_tags_form as set_tags_form %}{{ set_tags_form.media.css }}{% endwith %} + {% endif %} {% endblock %} {% block app_body %} -

{{ object.title }}

-

- {% if object|can_read_foiproject_authenticated:request %} - {% blocktrans with date=object.created|date:"SHORT_DATE_FORMAT" num=object.request_count %} +

{{ object.title }}

+

+ {% if object|can_read_foiproject_authenticated:request %} + {% blocktrans with date=object.created|date:"SHORT_DATE_FORMAT" num=object.request_count %} Created on {{ date }} with currently {{ num }} requests. {% endblocktrans %} - {% else %} - {% blocktrans with date=object.created|date:"SHORT_DATE_FORMAT" num=public_requests %} + {% else %} + {% blocktrans with date=object.created|date:"SHORT_DATE_FORMAT" num=public_requests %} Created on {{ date }} with currently {{ num }} public requests. {% endblocktrans %} - {% endif %} -

- {% if object|can_read_foiproject_authenticated:request and not all_public or not object.public %} -
-
-
-
- {% if object.public %} - {% if all_public %} - {% trans "This project and all its requests are public." %} - {% else %} - {% trans "This project is public, but some of its requests are not public." %} - {% endif %} - {% else %} - {% if all_non_public %} - {% trans "This project and all its requests are not public." %} - {% else %} - {% trans "This project is not public, but some of its requests are public." %} - {% endif %} - {% endif %} - {% if object|can_manage_foiproject:request %} -
- {% csrf_token %} - {% if not object.public and not all_public %} - {% render_form make_public_form %} - {% endif %} -
- -
-
- {% endif %} -
+ {% endif %} +

+ {% if object|can_read_foiproject_authenticated:request and not all_public or not object.public %} +
+
+
+
+ {% if object.public %} + {% if all_public %} + {% trans "This project and all its requests are public." %} + {% else %} + {% trans "This project is public, but some of its requests are not public." %} + {% endif %} + {% else %} + {% if all_non_public %} + {% trans "This project and all its requests are not public." %} + {% else %} + {% trans "This project is not public, but some of its requests are public." %} + {% endif %} + {% endif %} + {% if object|can_manage_foiproject:request %} +
+ {% csrf_token %} + {% if not object.public and not all_public %} + {% render_form make_public_form %} + {% endif %} +
+
-
+ + {% endif %} +
+
+
+ {% endif %} + {% if object|can_write_foiproject:request %} +
+ {% csrf_token %} + {% endif %} {% if object|can_write_foiproject:request %} - - {% csrf_token %} - +
+

{% translate "Perform action for selected requests:" %}

+ {% if object|can_manage_foiproject:request %} + {% endif %} - {% if object|can_write_foiproject:request %} -
-

{% translate "Perform action for selected requests:" %}

- {% if object|can_manage_foiproject:request %} - - {% endif %} - -
- {% endif %} -
- - - - - - - - {% if object|can_read_foiproject_authenticated:request %} - - {% endif %} - - - - {% for req in foirequests %} - - - - - - {% if object|can_read_foiproject_authenticated:request %} - - {% endif %} - - {% endfor %} - -
- {% if object|can_write_foiproject:request %} - - - {% else %} - # - {% endif %} - {% trans "status" %}{% trans "last message" %}{% trans "public body" %}{% trans "is public?" %}
- {% if object|can_write_foiproject:request %} - - - {% else %} - {{ req.project_number }} - {% endif %} - - {{ req.readable_status }} - - {{ req.last_message|date:"SHORT_DATE_FORMAT" }} - - {{ req.public_body.name }} - - {% if req.is_public %} - - {% translate "Yes" %} - {% else %} - - {% translate "No" %} - {% endif %} -
-
- {% if object|can_write_foiproject:request %}{% endif %} - {% if team_form %} - {% trans "Assign team to project" as legend %} - {% trans "Set team for project" as submit_button %} - {% url 'foirequest-project_set_team' slug=object.slug as submit_url %} - {% include "team/_assign_team_form.html" with object=object form=team_form submit_url=submit_url legend=legend submit_button=submit_button %} + +
{% endif %} +
+ + + + + + + + {% if object|can_read_foiproject_authenticated:request %} + + {% endif %} + + + + {% for req in foirequests %} + + + + + + {% if object|can_read_foiproject_authenticated:request %} + + {% endif %} + + {% endfor %} + +
+ {% if object|can_write_foiproject:request %} + + + {% else %} + # + {% endif %} + {% trans "status" %}{% trans "last message" %}{% trans "public body" %}{% trans "is public?" %}
+ {% if object|can_write_foiproject:request %} + + + {% else %} + {{ req.project_number }} + {% endif %} + + {{ req.readable_status }} + + {{ req.last_message|date:"SHORT_DATE_FORMAT" }} + + {{ req.public_body.name }} + + {% if req.is_public %} + + {% translate "Yes" %} + {% else %} + + {% translate "No" %} + {% endif %} +
+
+ {% if object|can_write_foiproject:request %}{% endif %} + {% if team_form %} + {% trans "Assign team to project" as legend %} + {% trans "Set team for project" as submit_button %} + {% url 'foirequest-project_set_team' slug=object.slug as submit_url %} + {% include "team/_assign_team_form.html" with object=object form=team_form submit_url=submit_url legend=legend submit_button=submit_button %} + {% endif %} {% endblock %} diff --git a/froide/foirequest/templates/foirequest/header/header.html b/froide/foirequest/templates/foirequest/header/header.html index 09b7283c1..d776b01eb 100644 --- a/froide/foirequest/templates/foirequest/header/header.html +++ b/froide/foirequest/templates/foirequest/header/header.html @@ -124,7 +124,12 @@
{% endif %} {% if object.summary %}
- {{ object.summary|urlizetrunc:40|linebreaks }} + {% redact_request_description object request as redacted_description %} + {{ redacted_description|linebreaks }} + + {% if object|can_write_foirequest:request %} + {% render_description_redact_button object %} + {% endif %}
+

+ +
+
+
+
diff --git a/froide/foirequest/templatetags/foirequest_tags.py b/froide/foirequest/templatetags/foirequest_tags.py index 29d597e42..9697af38c 100644 --- a/froide/foirequest/templatetags/foirequest_tags.py +++ b/froide/foirequest/templatetags/foirequest_tags.py @@ -5,6 +5,7 @@ from django import template from django.contrib.contenttypes.models import ContentType from django.db.models import Case, Value, When +from django.http import HttpRequest from django.template.defaultfilters import truncatechars_html from django.utils.html import format_html from django.utils.safestring import SafeString, mark_safe @@ -59,10 +60,10 @@ def highlight_request(message, request): real_content = unify(message.get_real_content()) redacted_content = unify(message.get_content()) - description = unify(message.request.description) + real_description = unify(message.request.description) redacted_description = unify(message.request.get_description()) description_with_markup = markup_redacted_content( - description, + real_description, redacted_description, authenticated_read=auth_read, message_id=message.id, @@ -70,8 +71,10 @@ def highlight_request(message, request): if auth_read: content = real_content + description = real_description else: content = redacted_content + description = redacted_description try: index = content.index(description) @@ -151,6 +154,24 @@ def redact_message(message, request): return content +@register.simple_tag +def redact_request_description( + foirequest: FoiRequest, request: HttpRequest +) -> SafeString: + authenticated_read = can_read_foirequest_authenticated(foirequest, request) + + real_content = unify(foirequest.description) + redacted_content = unify(foirequest.get_description()) + + content = mark_redacted( + real_content, + redacted_content, + authenticated_read=authenticated_read, + ) + + return content + + @register.simple_tag def redact_message_short(message, request): authenticated_read = is_authenticated_read(message, request) @@ -437,6 +458,23 @@ def render_message_redact_button(message): } +@register.inclusion_tag("foirequest/snippets/description_redact.html") +def render_description_redact_button(request): + return { + "foirequest": request, + "js_config": json.dumps( + { + "i18n": { + "subject": _("Subject"), + "message": _("Message"), + "messageLoading": _("Message is loading..."), + "blockedRedaction": _("This word needs to stay redacted."), + }, + } + ), + } + + @register.filter def readable_status(status, resolution=""): if status == FoiRequest.STATUS.RESOLVED and resolution: diff --git a/froide/foirequest/tests/test_web.py b/froide/foirequest/tests/test_web.py index 14cd9e9db..15273efe2 100644 --- a/froide/foirequest/tests/test_web.py +++ b/froide/foirequest/tests/test_web.py @@ -526,6 +526,7 @@ def test_queries_foirequest(world, client): """ FoiRequest page should query for non-loggedin users - FoiRequest (+1) + - Save FoiRequest redacted description (+1) - FoiRequest Tags (+1) - FoiMessages of that request (+1) - FoiAttachments of that request (+1) @@ -540,7 +541,7 @@ def test_queries_foirequest(world, client): mes2 = factories.FoiMessageFactory.create(request=req) factories.FoiAttachmentFactory.create(belongs_to=mes2) ContentType.objects.clear_cache() - with assertNumQueries(11): + with assertNumQueries(12): client.get(req.get_absolute_url()) @@ -550,6 +551,7 @@ def test_queries_foirequest_loggedin(world, client): FoiRequest page should query for non-staff loggedin users - Django session + Django user (+3) - FoiRequest (+1) + - Save FoiRequest redacted description (+1) - FoiRequest Tags (+1) - User and group permissions (+2) - FoiMessages of that request (+1) @@ -561,7 +563,7 @@ def test_queries_foirequest_loggedin(world, client): - Problem reports - even for non-requester (+1) - ContentType + Comments for each FoiMessage (+2) """ - TOTAL_EXPECTED_REQUESTS = 17 + TOTAL_EXPECTED_REQUESTS = 18 req = factories.FoiRequestFactory.create(site=world) factories.FoiMessageFactory.create(request=req, is_response=False) mes2 = factories.FoiMessageFactory.create(request=req) diff --git a/froide/foirequest/urls/request_urls.py b/froide/foirequest/urls/request_urls.py index 681c81de9..f332da53b 100644 --- a/froide/foirequest/urls/request_urls.py +++ b/froide/foirequest/urls/request_urls.py @@ -30,6 +30,7 @@ message_shortlink, publicbody_upload, redact_attachment, + redact_description, redact_message, resend_message, send_message, @@ -123,6 +124,11 @@ SetProjectView.as_view(), name="foirequest-set_project", ), + path( + "/redact-description/", + redact_description, + name="foirequest-redact_description", + ), # Messages path( "/add/postal-reply//", diff --git a/froide/foirequest/views/__init__.py b/froide/foirequest/views/__init__.py index 6adcf2f9c..0fc928d04 100644 --- a/froide/foirequest/views/__init__.py +++ b/froide/foirequest/views/__init__.py @@ -61,6 +61,7 @@ make_public, make_same_request, publicbody_upload, + redact_description, set_law, set_public_body, set_status, @@ -119,6 +120,7 @@ "suggest_public_body", "set_status", "make_public", + "redact_description", "set_law", "confirm_request", "delete_request", diff --git a/froide/foirequest/views/request_actions.py b/froide/foirequest/views/request_actions.py index 4ebaf8aee..5e259b927 100644 --- a/froide/foirequest/views/request_actions.py +++ b/froide/foirequest/views/request_actions.py @@ -13,6 +13,7 @@ from froide.account.forms import AddressForm, NewUserForm from froide.foirequest.forms.project import AssignProjectForm +from froide.foirequest.forms.request import RedactDescriptionForm from froide.helper.auth import can_manage_object from froide.helper.utils import get_redirect, is_ajax, render_400, render_403 from froide.team.views import AssignTeamView @@ -24,7 +25,7 @@ check_foirequest_upload_code, get_read_foirequest_queryset, ) -from ..decorators import allow_write_foirequest +from ..decorators import allow_write_foirequest, allow_write_or_moderate_pii_foirequest from ..forms import ( ApplyModerationForm, ConcreteLawForm, @@ -462,3 +463,18 @@ def publicbody_upload(request, obj_id, code): "foirequest/publicbody_upload.html", {"authenticated": True, "foirequest": foirequest, "config": config}, ) + + +@require_POST +@allow_write_or_moderate_pii_foirequest +def redact_description(request, foirequest): + form = RedactDescriptionForm(request.POST) + if form.is_valid(): + form.save(request, foirequest) + FoiEvent.objects.create_event( + FoiEvent.EVENTS.DESCRIPTION_REDACTED, + foirequest, + user=request.user, + **form.cleaned_data + ) + return redirect(foirequest.get_absolute_url()) diff --git a/froide/helper/text_utils.py b/froide/helper/text_utils.py index ae58e301d..17cbe629a 100644 --- a/froide/helper/text_utils.py +++ b/froide/helper/text_utils.py @@ -10,6 +10,8 @@ from slugify import slugify as _slugify +from .text_diff import get_diff_chunks + try: from lxml import html as html_parser from lxml.html import HtmlElement, HTMLParser @@ -326,3 +328,27 @@ def convert_element( repl_tag = html_parser.Element("span") repl_tag.text = replacement el.getparent().replace(el, repl_tag) + + +def apply_user_redaction(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, + ) diff --git a/frontend/javascript/components/messageredaction/description-redaction.vue b/frontend/javascript/components/messageredaction/description-redaction.vue new file mode 100644 index 000000000..c2403ca21 --- /dev/null +++ b/frontend/javascript/components/messageredaction/description-redaction.vue @@ -0,0 +1,69 @@ + + + diff --git a/frontend/javascript/messageredaction.js b/frontend/javascript/messageredaction.js index 4812509d6..1461baa34 100644 --- a/frontend/javascript/messageredaction.js +++ b/frontend/javascript/messageredaction.js @@ -1,11 +1,16 @@ import { createAppWithProps } from './lib/vue-helper' import MessageRedaction from './components/messageredaction/message-redaction.vue' +import DescriptionRedaction from './components/messageredaction/description-redaction.vue' function createMessageRedaction(selector) { createAppWithProps(selector, MessageRedaction).mount(selector) } +function createDescriptionRedaction(selector) { + createAppWithProps(selector, DescriptionRedaction).mount(selector) +} + document.querySelectorAll('[data-redact="message"]').forEach((el) => { el.addEventListener('show.bs.modal', () => { const redaction = el.querySelector('message-redaction') @@ -15,6 +20,15 @@ document.querySelectorAll('[data-redact="message"]').forEach((el) => { }) }) +document.querySelectorAll('[data-redact="description"]').forEach((el) => { + el.addEventListener('show.bs.modal', () => { + const redaction = el.querySelector('description-redaction') + if (redaction !== null) { + createDescriptionRedaction(redaction) + } + }) +}) + const exp = { createMessageRedaction } diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index db863089e..64e179c77 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -16,7 +16,7 @@ msgid "" msgstr "" "Project-Id-Version: froide\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-06 14:37+0100\n" +"POT-Creation-Date: 2023-11-07 14:49+0100\n" "PO-Revision-Date: 2023-10-09 16:55+0000\n" "Last-Translator: krmax44 \n" "Language-Team: German >" msgid "<>" msgstr "<>" +#: froide/helper/text_utils.py +msgid "[redacted]" +msgstr "[geschwärzt]" + #: froide/letter/apps.py msgid "Letter" msgstr "Brief"