diff --git a/froide/account/forms.py b/froide/account/forms.py index 8b6a493c5..870cb741f 100644 --- a/froide/account/forms.py +++ b/froide/account/forms.py @@ -12,7 +12,6 @@ from django.http import HttpRequest from django.utils.functional import SimpleLazyObject from django.utils.html import format_html -from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from froide.helper.content_urls import get_content_url @@ -20,6 +19,7 @@ from froide.helper.spam import SpamProtectionMixin from froide.helper.widgets import ( BootstrapCheckboxInput, + BootstrapRadioSelect, BootstrapSelect, ImageFileInput, ) @@ -32,6 +32,7 @@ from .widgets import ConfirmationWidget, PinInputWidget USER_CAN_HIDE_WEB = settings.FROIDE_CONFIG.get("user_can_hide_web", True) +USER_CAN_CLAIM_VIP = settings.FROIDE_CONFIG.get("user_can_claim_vip", False) ALLOW_PSEUDONYM = settings.FROIDE_CONFIG.get("allow_pseudonym", False) @@ -92,6 +93,7 @@ def get_user(self): def clean_address(self) -> str: address = self.cleaned_data["address"] + # TODO validate like clientside? cf. addressRegex in user-address.vue if not address: return address if self.ALLOW_BLOCKED_ADDRESS: @@ -134,18 +136,41 @@ class NewUserBaseForm(AddressBaseForm): ), ) + if USER_CAN_CLAIM_VIP: + claims_vip = forms.TypedChoiceField( + required=False, + initial=False, + widget=BootstrapRadioSelect, + label=_("{site_name} for journalists").format(site_name=settings.SITE_NAME), + help_text=_( + _( + "You work in journalism and would like to use {site_name} for your research? Shortly after your sign-up is completed, we will send you additional information about extra functionality for journalists." + ).format(site_name=settings.SITE_NAME) + ), + choices=[ + (False, _("No, I am not a journalist")), + (True, _("Yes, I am a journalist")), + ], + coerce=lambda x: x != "False", + ) + ALLOW_BLOCKED_ADDRESS = True if USER_CAN_HIDE_WEB: - private = forms.BooleanField( + private = forms.TypedChoiceField( required=False, - widget=BootstrapCheckboxInput, + widget=BootstrapRadioSelect, label=_("Hide my name from public view"), - help_text=mark_safe( - _( - "If you check this, your name will still appear in requests to public bodies, but we will do our best to not display it publicly. However, we cannot guarantee your anonymity" - ) - ), + choices=[ + ( + False, + _( + "My name may appear on the website in plain text" + ), + ), + (True, _("My name must be redacted")), + ], + coerce=lambda x: x != "False", ) field_order = ["first_name", "last_name", "user_email"] diff --git a/froide/account/templates/account/new.html b/froide/account/templates/account/new.html index 86225e598..a78c215ee 100644 --- a/froide/account/templates/account/new.html +++ b/froide/account/templates/account/new.html @@ -1,6 +1,7 @@ {% extends 'account/base.html' %} {% load i18n %} {% load static %} +{% load frontendbuild %} {% block title %} {% trans "New account" %} {% endblock %} diff --git a/froide/account/tests/test_account.py b/froide/account/tests/test_account.py index 11958d541..5ee1eae43 100644 --- a/froide/account/tests/test_account.py +++ b/froide/account/tests/test_account.py @@ -127,6 +127,7 @@ def test_signup(world, client): "first_name": "Horst", "last_name": "Porst", "terms": "on", + "private": True, "user_email": "horst.porst", "time": (datetime.now(timezone.utc) - timedelta(seconds=30)).timestamp(), } @@ -180,6 +181,7 @@ def test_overlong_name_signup(world, client): "first_name": "Horst" * 6 + "a", "last_name": "Porst" * 6, "terms": "on", + "private": True, "user_email": "horst.porst@example.com", "address": "MyOwnPrivateStree 5\n31415 Pi-Ville", "time": (datetime.now(timezone.utc) - timedelta(seconds=30)).timestamp(), @@ -199,6 +201,7 @@ def test_signup_too_fast(world, client): "first_name": "Horst", "last_name": "Porst", "terms": "on", + "private": True, "user_email": "horst.porst@example.com", "address": "MyOwnPrivateStree 5\n31415 Pi-Ville", # Signup in less than 5 seconds @@ -216,6 +219,7 @@ def test_signup_same_name(world, client): "first_name": "Horst", "last_name": "Porst", "terms": "on", + "private": True, "user_email": "horst.porst@example.com", "address": "MyOwnPrivateStree 5\n31415 Pi-Ville", "time": (datetime.now(timezone.utc) - timedelta(seconds=30)).timestamp(), @@ -308,6 +312,7 @@ def test_next_link_signup(world, client): "first_name": "Horst", "last_name": "Porst", "terms": "on", + "private": True, "user_email": "horst.porst@example.com", "address": "MyOwnPrivateStree 5\n31415 Pi-Ville", "next": url, @@ -439,7 +444,7 @@ def test_private_name(world, client): post = { "subject": "Request - Private name", "body": "This is a test body", - "public": "on", + "public": True, "publicbody": pb.pk, "law": pb.default_law.pk, } @@ -787,6 +792,7 @@ def test_signup_blocklisted(world, client): "first_name": "Horst", "last_name": "Porst", "terms": "on", + "private": True, "user_email": "horst.porst@example.com", "time": (datetime.now(timezone.utc) - timedelta(seconds=30)).timestamp(), } diff --git a/froide/account/views.py b/froide/account/views.py index 88ef5d451..770ae37b4 100644 --- a/froide/account/views.py +++ b/froide/account/views.py @@ -290,7 +290,10 @@ def get_context_data(self, **kwargs): @require_POST def logout(request: HttpRequest) -> HttpResponseRedirect: auth.logout(request) - messages.add_message(request, messages.INFO, _("You have been logged out.")) + # we use the extra_tags to communicate a logout to purgestorage.ts (via main.ts) + messages.add_message( + request, messages.INFO, _("You have been logged out."), "info alert-loggedout" + ) return redirect("/") diff --git a/froide/campaign/migrations/0008_campaign_logo_campaign_short_description.py b/froide/campaign/migrations/0008_campaign_logo_campaign_short_description.py new file mode 100644 index 000000000..0b49d3c75 --- /dev/null +++ b/froide/campaign/migrations/0008_campaign_logo_campaign_short_description.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.6 on 2025-12-08 11:55 + +import froide.helper.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("campaign", "0007_campaign_report_url"), + ] + + operations = [ + migrations.AddField( + model_name="campaign", + name="logo", + field=models.ImageField( + blank=True, + null=True, + storage=froide.helper.storage.HashedFilenameStorage(), + upload_to="campaign-logos", + ), + ), + migrations.AddField( + model_name="campaign", + name="short_description", + field=models.TextField(blank=True), + ), + ] diff --git a/froide/campaign/models.py b/froide/campaign/models.py index 453e9a6bd..46d585f1c 100644 --- a/froide/campaign/models.py +++ b/froide/campaign/models.py @@ -6,6 +6,8 @@ from django.template.loader import select_template from django.utils.translation import gettext_lazy as _ +from froide.helper.storage import HashedFilenameStorage + class CampaignManager(models.Manager): def get_filter_list(self) -> QuerySet: @@ -24,9 +26,16 @@ class Campaign(models.Model): url = models.URLField(blank=True) report_url = models.URLField(blank=True) description = models.TextField(blank=True) + short_description = models.TextField(blank=True) start_date = models.DateTimeField(null=True) public = models.BooleanField(default=False) active = models.BooleanField(default=False) + logo = models.ImageField( + null=True, + blank=True, + upload_to="campaign-logos", + storage=HashedFilenameStorage(), + ) request_match = models.TextField(blank=True) request_hint = models.TextField(blank=True) diff --git a/froide/foirequest/api_views/request.py b/froide/foirequest/api_views/request.py index e7594f5ef..e7422df26 100644 --- a/froide/foirequest/api_views/request.py +++ b/froide/foirequest/api_views/request.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.db.models import Q +from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as filters from rest_framework import mixins, permissions, status, throttling, viewsets @@ -80,7 +81,7 @@ class FoiRequestFilter(filters.FilterSet): campaign = filters.ModelChoiceFilter( queryset=Campaign.objects.filter(public=True), null_value="-", - null_label="No Campaign", + null_label=_("no campaign"), lookup_expr="isnull", method="campaign_filter", ) diff --git a/froide/foirequest/filters.py b/froide/foirequest/filters.py index bf95cd77b..c57658125 100644 --- a/froide/foirequest/filters.py +++ b/froide/foirequest/filters.py @@ -1,4 +1,5 @@ from collections import namedtuple +from functools import cache from django import forms from django.utils import timezone @@ -137,6 +138,16 @@ def get_status_filter_by_slug(slug): return status_filter +# jurisdictions seldomly change, so it's okay to cache until app restart +@cache +def get_jurisdictions_by_rank(rank: int) -> list[int]: + return list( + Jurisdiction.objects.get_visible() + .filter(rank=rank) + .values_list("id", flat=True) + ) + + class DropDownStatusFilterWidget(DropDownFilterWidget): def create_option( self, name, value, label, selected, index, subindex=None, attrs=None @@ -177,6 +188,9 @@ class BaseFoiRequestFilterSet(BaseSearchFilterSet): widget=BootstrapSelect, method="filter_jurisdiction", ) + jurisdiction_rank = django_filters.NumberFilter( + label=("jurisdiction rank"), method="filter_jurisdiction_rank" + ) category = django_filters.ModelChoiceFilter( queryset=Category.objects.get_category_list(), to_field_name="slug", @@ -234,6 +248,7 @@ class BaseFoiRequestFilterSet(BaseSearchFilterSet): last = django_filters.DateFromToRangeFilter( method="filter_last", widget=DateRangeWidget, label=_("last message") ) + sort = django_filters.ChoiceFilter( choices=[ ("-last", _("last message (newest first)")), @@ -253,6 +268,7 @@ class Meta: "q", "status", "jurisdiction", + "jurisdiction_rank", "campaign", "category", "classification", @@ -274,6 +290,11 @@ def filter_status(self, qs, name, value): def filter_jurisdiction(self, qs, name, value): return self.apply_filter(qs, name, jurisdiction=value.id) + def filter_jurisdiction_rank(self, qs, name, value): + return self.apply_filter( + qs, name, jurisdiction=get_jurisdictions_by_rank(int(value)) + ) + def filter_campaign(self, qs, name, value): if value == "-": return self.apply_filter( diff --git a/froide/foirequest/forms/preferences.py b/froide/foirequest/forms/preferences.py index 2c25203a8..2ba431fc3 100644 --- a/froide/foirequest/forms/preferences.py +++ b/froide/foirequest/forms/preferences.py @@ -3,7 +3,7 @@ from froide.account.preferences import PreferenceForm, registry -class RequestPageTourForm(PreferenceForm): +class BooleanPreferenceForm(PreferenceForm): value = forms.TypedChoiceField( widget=forms.HiddenInput, choices=( @@ -15,13 +15,17 @@ class RequestPageTourForm(PreferenceForm): request_page_tour_pref = registry.register( - "foirequest_requestpage_tour", RequestPageTourForm + "foirequest_requestpage_tour", BooleanPreferenceForm ) message_received_tour_pref = registry.register( - "foirequest_messagereceived_tour", RequestPageTourForm + "foirequest_messagereceived_tour", BooleanPreferenceForm ) postal_reply_tour_pref = registry.register( - "foirequest_postalreply_tour", RequestPageTourForm + "foirequest_postalreply_tour", BooleanPreferenceForm +) + +make_request_intro_skip_howto_pref = registry.register( + "foirequest_skiphowto_make", BooleanPreferenceForm ) diff --git a/froide/foirequest/forms/request.py b/froide/foirequest/forms/request.py index 0e9f80d70..ff44375cd 100644 --- a/froide/foirequest/forms/request.py +++ b/froide/foirequest/forms/request.py @@ -60,15 +60,28 @@ class RequestForm(JSONMixin, forms.Form): label=_("Don't wrap in template"), widget=forms.CheckboxInput(attrs={"tabindex": "-1"}), ) - public = forms.BooleanField( - required=False, - initial=True, + public = forms.TypedChoiceField( label=_("This request is public."), - help_text=_( - "If you don't want your request to be public right now," - " uncheck this. You can always decide to make it public later." - ), + initial=True, + required=False, + widget=BootstrapRadioSelect, + choices=[ + ( + True, + _( + "I want the request to be immediately accessible to the public on this website. (Default)" + ), + ), + ( + False, + _( + "I want the request to remain not public for now, and publish it later." + ), + ), + ], + coerce=lambda x: x != "False", ) + reference = forms.CharField(widget=forms.HiddenInput, required=False) law_type = forms.CharField(widget=forms.HiddenInput, required=False) redirect_url = forms.CharField(widget=forms.HiddenInput, required=False) diff --git a/froide/foirequest/models/request.py b/froide/foirequest/models/request.py index b278a7e98..d9f0fff6b 100644 --- a/froide/foirequest/models/request.py +++ b/froide/foirequest/models/request.py @@ -553,11 +553,25 @@ def status_representation(self): def status_settable(self): return self.awaits_classification() + @property + def jurisdiction_name(self): + if self.jurisdiction: + return self.jurisdiction.name + @property def project_number(self): - if self.project_order is not None: + if self.project_order: return self.project_order + 1 - return None + + @property + def project_site_url(self): + if self.project: + return self.project.get_absolute_domain_url() + + @property + def project_request_count(self): + if self.project: + return self.project.request_count @property def has_fee(self): diff --git a/froide/foirequest/serializers.py b/froide/foirequest/serializers.py index 3f33cd45c..c6f80029b 100644 --- a/froide/foirequest/serializers.py +++ b/froide/foirequest/serializers.py @@ -80,6 +80,7 @@ class Meta: "id", "url", "jurisdiction", + "jurisdiction_name", "is_foi", "checked", "refusal_reason", @@ -97,6 +98,8 @@ class Meta: "created_at", "last_modified_at", "status", + "status_representation", + "readable_status", "public_body", "resolution", "slug", @@ -104,6 +107,8 @@ class Meta: "reference", "user", "project", + "project_site_url", + "project_request_count", "campaign", "tags", ) @@ -118,12 +123,17 @@ class Meta: "last_message", "created_at", "last_modified_at", + "status_representation", + "readable_status", "public_body", "slug", "title", "reference", "user", + "jurisdiction_name", "project", # TODO: make this updatable + "project_site_url", # TODO: for api v2, create a FoiProject serializer and possibly inline it here + "project_request_count", "campaign", ) diff --git a/froide/foirequest/templates/foirequest/request.html b/froide/foirequest/templates/foirequest/request.html index a0da34fb5..21d4c485a 100644 --- a/froide/foirequest/templates/foirequest/request.html +++ b/froide/foirequest/templates/foirequest/request.html @@ -18,27 +18,15 @@ {% block messages %} {% endblock messages %} {% block body %} - {% block before_form %} - {% if campaigns %} -
-
-

{% trans "Join our current FOI campaigns" %}

-
- {% for campaign in campaigns %} -
-

{{ campaign.name }}

- {% trans "Make a request" %} -
- {% endfor %} -
-
-
- {% endif %} - {% endblock %} + {% block before_form %}{% endblock %}
+ {% comment %} + action="{% url 'foirequest-make_request' %}" + {% endcomment %} {% csrf_token %} {{ request_form.reference }} {{ request_form.tags }} @@ -48,10 +36,9 @@

{{ campaign.name }}

{% for k, v in config.items %} {% if v %}{% endif %} {% endfor %} - {{ campaign.name }} {% if config.hide_editing %}:hide-editing="true"{% endif %} {% if config.hide_public %}:hide-public="true"{% endif %} {% if multi_request %}:multi-request="true"{% endif %} - {% if beta_ui %}:beta-ui="true"{% endif %} + {% if confirm_required %}:confirm-required="true"{% endif %} :config="{{ js_config }}"> + + + + + + + + +
diff --git a/froide/foirequest/templates/foirequest/sent.html b/froide/foirequest/templates/foirequest/sent.html index 2353b5051..9708c6aac 100644 --- a/froide/foirequest/templates/foirequest/sent.html +++ b/froide/foirequest/templates/foirequest/sent.html @@ -1,11 +1,12 @@ {% extends "foirequest/base.html" %} {% load i18n %} {% load static %} +{% load frontendbuild %} {% block title %} {% trans "Your request has been sent" %} {% endblock title %} {% block body %} -
+
{% trans "Write a request" %} diff --git a/froide/foirequest/templates/foirequest/snippets/request_campaign_other.html b/froide/foirequest/templates/foirequest/snippets/request_campaign_other.html new file mode 100644 index 000000000..e5c99fcd0 --- /dev/null +++ b/froide/foirequest/templates/foirequest/snippets/request_campaign_other.html @@ -0,0 +1 @@ +{# intentionally blank #} diff --git a/froide/foirequest/templates/foirequest/snippets/request_hints.html b/froide/foirequest/templates/foirequest/snippets/request_hints.html new file mode 100644 index 000000000..69d867f28 --- /dev/null +++ b/froide/foirequest/templates/foirequest/snippets/request_hints.html @@ -0,0 +1,24 @@ +{% load i18n %} +

{% blocktrans %}Important Notes:{% endblocktrans %}

+
    +
  • + {% blocktrans %}Write your request in simple, precise language.{% endblocktrans %} +
  • +
  • + {% blocktrans %}Ask for specific documents or information.{% endblocktrans %} +
  • +
  • + {% blocktrans %}Keep it focused and concise.{% endblocktrans %} +
  • +
  • + {% blocktrans %}This site is public. Everything you type and any response will be published.{% endblocktrans %} +
  • +
  • + {% blocktrans %}Do not include personal information in your request.{% endblocktrans %} +
  • +
  • + {% blocktrans %}Do not ask for personal information.{% endblocktrans %} +
  • +
  • {% blocktrans %}Please use proper spelling.{% endblocktrans %}
  • +
  • {% blocktrans %}Please stay polite.{% endblocktrans %}
  • +
diff --git a/froide/foirequest/templates/foirequest/snippets/request_intro_howto.html b/froide/foirequest/templates/foirequest/snippets/request_intro_howto.html new file mode 100644 index 000000000..1f009eceb --- /dev/null +++ b/froide/foirequest/templates/foirequest/snippets/request_intro_howto.html @@ -0,0 +1 @@ +{# intentionally blank; page-request.vue will skip the INTRO_HOWTO step #} diff --git a/froide/foirequest/templates/foirequest/snippets/request_user_confirm.html b/froide/foirequest/templates/foirequest/snippets/request_user_confirm.html new file mode 100644 index 000000000..db1b9706d --- /dev/null +++ b/froide/foirequest/templates/foirequest/snippets/request_user_confirm.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}I confirm that I am not requesting personal information about myself.{% endblocktrans %} diff --git a/froide/foirequest/tests/test_draft.py b/froide/foirequest/tests/test_draft.py index 187b56aec..527d234c7 100644 --- a/froide/foirequest/tests/test_draft.py +++ b/froide/foirequest/tests/test_draft.py @@ -23,6 +23,8 @@ def test_draft_not_loggedin(self): "last_name": "Wehrmeyer", "user_email": "dummy@example.com", "terms": "on", + "private": True, + "public": True, "save_draft": "true", "publicbody": str(self.pb.pk), } @@ -60,7 +62,7 @@ def test_draft_logged_in(self): post = { "subject": "Test-Subject", "body": "This is another test body with Ümläut€n", - "public": "on", + "public": True, "reference": "test:abcdefg", "save_draft": "true", "publicbody": str(self.pb.pk), @@ -151,6 +153,7 @@ def test_draft_token(self): "last_name": "Wehrmeyer", "user_email": user.email, "terms": "on", + "public": True, "publicbody": str(self.pb.pk), "hide_editing": "1", "hide_similar": "1", diff --git a/froide/foirequest/tests/test_project.py b/froide/foirequest/tests/test_project.py index d08b30a87..da5a9b251 100644 --- a/froide/foirequest/tests/test_project.py +++ b/froide/foirequest/tests/test_project.py @@ -40,7 +40,7 @@ def test_create_project(self): data = { "subject": "Test-Subject", "body": "This is another test body with Ümläut€n", - "public": "on", + "public": True, "publicbody": pb_ids.split("+"), } mail.outbox = [] @@ -81,7 +81,7 @@ def test_create_project_full_text(self): data = { "subject": "Test-Subject", "body": "This is another test body with Ümläut€n", - "public": "on", + "public": True, "publicbody": pb_ids, "full_text": "on", } @@ -122,7 +122,7 @@ def test_draft_project(self): data = { "subject": "Test-Subject", "body": "This is another test body with Ümläut€n", - "public": "on", + "public": True, "publicbody": pb_ids + [evil_pb3], "draft": draft.pk, } diff --git a/froide/foirequest/tests/test_request.py b/froide/foirequest/tests/test_request.py index 80a638822..dfde4e342 100644 --- a/froide/foirequest/tests/test_request.py +++ b/froide/foirequest/tests/test_request.py @@ -72,6 +72,7 @@ def test_public_body_logged_in_request(world, client, pb): "subject": "Test-Subject", "body": "This is another test body with Ümläut€n", "publicbody": pb.pk, + "public": False, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -114,6 +115,8 @@ def test_public_body_new_user_request(world, client, pb): "address": "TestStreet 3\n55555 Town", "user_email": "sw@example.com", "terms": "on", + "public": False, + "private": True, "publicbody": pb.pk, } response = client.post(reverse("foirequest-make_request"), post) @@ -410,7 +413,7 @@ def test_logged_in_request_with_public_body(world, client, pb): "subject": "Another Third Test-Subject", "body": "This is another test body", "publicbody": "bs", - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 400 @@ -459,7 +462,7 @@ def test_redirect_after_request(world, client, pb): "body": "This is another test body", "redirect_url": "/foo/?blub=bla", "publicbody": str(pb.pk), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -473,7 +476,7 @@ def test_redirect_after_request(world, client, pb): "body": "This is another test body", "redirect_url": "http://evil.example.com", "publicbody": str(pb.pk), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) request_sent = reverse("foirequest-request_sent") @@ -490,7 +493,8 @@ def test_redirect_after_request_new_account(world, client, pb): "body": "This is another test body", "redirect_url": redirect_url, "publicbody": str(pb.pk), - "public": "on", + "public": True, + "private": True, "first_name": "Stefan", "last_name": "Wehrmeyer", "address": "TestStreet 3\n55555 Town", @@ -525,7 +529,7 @@ def test_foi_email_settings(world, client, pb, settings): "body": "This is another test body", "publicbody": str(pb.pk), "law": str(pb.default_law.pk), - "public": "on", + "public": True, } def email_func(username, secret): @@ -645,7 +649,7 @@ def test_postal_reply(world, client, pb): "subject": "Totally Random Request", "body": "This is another test body", "publicbody": str(pb.pk), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -837,7 +841,7 @@ def test_set_message_sender(world, client, pb, msgobj): "subject": "A simple test request", "body": "This is another test body", "publicbody": str(pb.id), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -1465,6 +1469,8 @@ def test_make_same_request(world, client): "address": "MyAddres 12\nB-Town", "user_email": "bob@example.com", "terms": "on", + "public": True, + "private": True, } response = client.post( reverse("foirequest-make_same_request", kwargs={"slug": same_req.slug}), @@ -1573,7 +1579,7 @@ def test_full_text_request(world, client, pb): "body": "This is another test body with Ümläut€n", "full_text": "true", "publicbody": str(pb.id), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -1826,6 +1832,7 @@ def test_too_long_subject(world, client, pb): "subject": "Test" * 64, "body": "This is another test body with Ümläut€n", "publicbody": pb.pk, + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 400 @@ -1834,6 +1841,7 @@ def test_too_long_subject(world, client, pb): "subject": "Test" * 55 + " a@b.de", "body": "This is another test body with Ümläut€n", "publicbody": pb.pk, + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -1882,7 +1890,7 @@ def test_throttling(world, client, pb, request_throttle): "subject": "Another Third Test-Subject", "body": "This is another test body", "publicbody": str(pb.pk), - "public": "on", + "public": True, } post["law"] = str(pb.default_law.pk) @@ -2082,6 +2090,7 @@ def test_letter_public_body(world, client, pb): "subject": "Jurisdiction-Test-Subject", "body": "This is a test body", "publicbody": pb.pk, + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -2124,7 +2133,7 @@ def test_postal_after_last(world, client, pb, faker): "subject": "Totally Random Request", "body": "This is another test body", "publicbody": str(pb.pk), - "public": "on", + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 @@ -2166,6 +2175,7 @@ def test_mail_confirmation_after_success(world, user, client, faker): "subject": faker.text(max_nb_chars=50), "body": faker.text(max_nb_chars=500), "publicbody": pb.pk, + "public": True, } client.login(email=user.email, password="froide") @@ -2267,6 +2277,7 @@ def test_request_body_leading_indent(world, client, pb): "subject": "Test-Subject", "body": " 1. Indented\n 2. Indented", "publicbody": pb.pk, + "public": True, } response = client.post(reverse("foirequest-make_request"), post) assert response.status_code == 302 diff --git a/froide/foirequest/views/make_request.py b/froide/foirequest/views/make_request.py index e5a8f76f5..3f387a5b6 100644 --- a/froide/foirequest/views/make_request.py +++ b/froide/foirequest/views/make_request.py @@ -4,11 +4,11 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import Http404 +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse +from django.utils import timezone from django.utils.decorators import decorator_from_middleware, method_decorator -from django.utils.html import linebreaks from django.utils.module_loading import import_string from django.utils.translation import gettext as _ from django.utils.translation import pgettext @@ -16,10 +16,13 @@ from django.views.generic import DetailView, FormView, TemplateView from froide.account.forms import AddressForm, NewUserForm +from froide.account.preferences import get_preferences_for_user from froide.campaign.models import Campaign +from froide.foirequest.forms.preferences import make_request_intro_skip_howto_pref from froide.georegion.models import GeoRegion from froide.helper.auth import get_read_queryset -from froide.helper.utils import update_query_params +from froide.helper.content_urls import get_content_url +from froide.helper.utils import is_fetch, update_query_params from froide.proof.forms import ProofMessageForm from froide.publicbody.forms import MultiplePublicBodyForm, PublicBodyForm from froide.publicbody.models import PublicBody @@ -132,14 +135,37 @@ def get_user_template_vars(self): return user_vars def get_js_context(self): + skip_intro_howto = False + show_skip_intro_howto_preference = False + if self.request.user.is_authenticated: + show_skip_intro_howto_preference = True + preference = get_preferences_for_user( + self.request.user, [make_request_intro_skip_howto_pref] + ) + preference_value = preference[make_request_intro_skip_howto_pref.key].value + if preference_value is not None: + skip_intro_howto = preference_value + elif FoiRequest.objects.filter(user=self.request.user).count() > 5: + skip_intro_howto = True + if first_request := FoiRequest.objects.order_by("created_at").first(): + min_year = first_request.created_at.year + else: + min_year = timezone.now().year + ctx = { "settings": { "user_can_hide_web": settings.FROIDE_CONFIG.get("user_can_hide_web"), + "user_can_claim_vip": settings.FROIDE_CONFIG.get("user_can_claim_vip"), "user_can_create_batch": self.can_create_batch(), "non_meaningful_subject_regex": settings.FROIDE_CONFIG.get( "non_meaningful_subject_regex", [] ), + # FIXME? default for address_regex is None, which will not become "" here "address_regex": settings.FROIDE_CONFIG.get("address_regex", ""), + "skip_intro_howto": skip_intro_howto, + "show_skip_intro_howto_preference": show_skip_intro_howto_preference, + "skip_intro_howto_preference_key": make_request_intro_skip_howto_pref.key, + "min_year": min_year, }, "url": { "searchRequests": reverse("api:request-search"), @@ -155,6 +181,11 @@ def get_js_context(self): "foirequest-make_request", kwargs={"publicbody_ids": "0"} ), "makeRequest": reverse("foirequest-make_request"), + "helpRequestPublic": get_content_url("help_request_public"), + "helpRequestPrivacy": get_content_url("help_request_privacy"), + "login": "{}?next={}".format( + reverse("account-login"), self.request.get_full_path() + ), }, "i18n": { "publicBodiesFound": [ @@ -176,16 +207,46 @@ def get_js_context(self): # Translators: not url "requests": _("requests"), "close": _("close"), + "back": _("Back"), + "remove": _("Remove"), + "stepNext": _("Next"), + "stepSkip": _("Skip"), + "step": _("Step"), + "introduction": _("Introduction"), + "similarRequests": _("Similar requests"), + "findSimilarRequests": _("Find similar requests"), + "currentlyChosen": _("Currently chosen:"), + "requestVisibility": _("Public visibility of the request"), + "previewAndSubmit": _("Preview & submit"), + "address": _("Address"), + "account": pgettext("Make request breadcrumbs/stepper", "Account"), + "writeMessage": _("Write message"), + "submitRequest": _("Submit request"), "makeRequest": _("Make request"), + "makeRequestYourself": _("Make a request yourself"), + "whatDoYouWantToDo": _("What do you want to do?"), + "whatCanIRequest": _("What can I request?"), + "whatCanINotRequest": _("What can’t I request?"), + "search": _("Search"), + "searchArchive": _("Search our archives:"), + "doYouAlreadyHaveAccount": _("Do you already have an account?"), + "thisFormRemembers": _( + "This form remembers your inputs, as long as stay in the same tab." + ), "writingRequestTo": _("You are writing a request to"), "toMultiPublicBodies": _("To: {count} public bodies").format( count="${count}" ), "selectPublicBodies": _("Select public bodies"), "continue": _("continue"), + "continueWithSelected": _("Continue with selected"), "selectAll": [_("select one"), _("select all")], "selectingAll": _("Selecting all public bodies, please wait..."), "name": _("Name"), + "filterPlaceholder": _("Filter…"), + "level": pgettext("Publicbody filter", "Level"), + "location": pgettext("Publicbody filter", "Location"), + "groupBy_state": _("state"), "jurisdictionPlural": [ _("Jurisdiction"), _("Jurisdictions"), @@ -245,9 +306,6 @@ def get_js_context(self): "reviewFrom": _("From"), "reviewTo": _("To"), "reviewPublicbodies": _("public bodies"), - "reviewSpelling": _("Please use proper spelling."), - "reviewPoliteness": _("Please stay polite."), - "submitRequest": _("Submit request"), "greeting": _("Dear Sir or Madam"), "kindRegards": _("Kind regards"), "yourFirstName": _("Your first name"), @@ -258,7 +316,6 @@ def get_js_context(self): "similarExist": _( "Please make sure the information is not already requested or public" ), - "similarRequests": _("Similar requests"), "moreSimilarRequests": _("Search for more similar requests"), "relevantResources": _("Relevant resources"), "officialWebsite": _("Official website: "), @@ -276,14 +333,92 @@ def get_js_context(self): "enterMeaningfulSubject": _( "Please enter a subject which describes the information you are requesting." ), - "pleaseFollowAddressFormat": linebreaks( - _( - "Please enter an address in the following format:\n%(format)s", - ) - % {"format": _("Street address,\nPost Code, City")} - ), + "pleaseFollowAddressFormat": _( + "Please enter an address in the following format:\n%(format)s", + ) + % {"format": _("Street address,\nPost Code, City")}, "includeProof": _("Attach a proof of identity"), "addMoreAuthorities": _("Add more authorities"), + # mimic Django's default messages, but use magic {count} parameter + # "Ensure this value has at least %(limit_value)d characters (it has %(show_value)d)." + "valueMinLength": [ + _("Ensure this value has at least {count} character.").format( + count="${count}" + ), + _("Ensure this value has at least {count} characters.").format( + count="${count}" + ), + ], + "valueMaxLength": [ + _("Ensure this value has at most {count} character.").format( + count="${count}" + ), + _("Ensure this value has at most {count} characters).").format( + count="${count}" + ), + ], + "results": [ + _("One result"), + _("{count} results").format(count="${count}"), + ], + "createAccount": _("Create account"), + "createAccountPreamble": _( + "You need to create an account at {site_name} so you will be able to manage your request." + ).format(site_name=settings.SITE_NAME), + "skipIntroHowto": _( + "Skip this part next time and start at “Choose public body” (optional)" + ), + "error": _("Error"), + "none": _("None chosen"), + "toPb": _("To:"), + "to": _("to"), + "requestTo": _("Request to:"), + "correct": _("Correct"), + "fixPlaceholder": _("Fix placeholders automatically"), + "updatePostalAddress": _("Update my postal address"), + "subjectMeaningful": _("Please write a meaningful subject"), + "containsPlaceholderMarker": _( + "The body contains an illegal placeholder ({placeholder})." + ).format(placeholder="${placeholder}"), + "pagination": _("Pagination"), + "publicbody": _("Public body"), + "requestBody": _("Request body"), + "request": _("request"), + "visibility": _("Visibility"), + "notConfirmed": _("Not confirmed"), + "notAgreed": _("Not agreed"), + "publishNow": _("Publish request now"), + "publishLater": _("Publish request later"), + "help": _("Help"), + "privacy": _("Privacy"), + "privacyMoreInfo": _("More information about privacy on this website"), + "nameRedact": _("My name must be redacted"), + "namePlainText": _( + "My name may appear on the website in plain text" + ), + "email": _("Email"), + "notSentToPb": _("(not sent to public body)"), + "terms": _("Terms of Use"), + "searchToPbName": _("to %(name)s") % {"name": "${name}"}, + "searchToProject": _( + 'to %(name)s and %(count)s other public bodies' + ) + % { + "name": "${name}", + "url": "${url}", + "urlp": "${urlp}", + "count": "${count}", + }, + "notYetSet": _("Not yet set"), + "searchSelectThisPb": _("Select this public body"), + "dateRange": _("Date range"), + "dateRangeFrom": _("From"), + "dateRangeTo": _("Until"), + "campaign": _("Campaign"), + "toggleCollapse": _("Toggle collapse"), + "searchText": pgettext("Search input", "Text"), + "login": pgettext("Make request", "Log in"), + "searchRequests": _("Search requests"), }, "regex": { "greetings": [_("Dear Sir or Madam")], @@ -296,6 +431,10 @@ def get_js_context(self): if k in settings.FROIDE_CONFIG.get("filter_georegion_kinds", []) ], }, + "draftId": self.object.id + if hasattr(self, "object") and isinstance(self.object, RequestDraft) + else None, + "wasPost": self.request.method == "POST", } pb_ctx = get_widget_context() for key in pb_ctx: @@ -482,15 +621,29 @@ def post(self, request, *args, **kwargs): if not user_form.is_valid(): error = True + proof_form = self.get_proof_form() + form_kwargs = { "request_form": request_form, "user_form": user_form, "publicbody_form": publicbody_form, - "proof_form": self.get_proof_form(), + "proof_form": proof_form, } if not error: return self.form_valid(**form_kwargs) + + if is_fetch(request): + return JsonResponse( + { + # "json serializeable form_kwargs subset" + "request_form": request_form.as_data(), + "user_form": user_form.as_data() if user_form else None, + "proof_form": proof_form.as_data() if proof_form else None, + }, + status=400, + ) + return self.form_invalid(**form_kwargs) def save_draft(self, request_form, publicbody_form): @@ -543,6 +696,11 @@ def form_valid( service = CreateRequestService(data) foi_object = service.execute(self.request) + # user just created + if not user.is_authenticated and foi_object.user and "claims_vip" in data: + if data["claims_vip"]: + foi_object.user.tags.add("claims:vip") + return self.make_redirect( request_form, foi_object, email=data.get("user_email") ) @@ -620,17 +778,18 @@ def get_context_data(self, **kwargs): if self.request.GET.get("single") is not None: is_multi = False - if self.request.method == "POST" or publicbodies or is_multi: - campaigns = None - else: - campaigns = Campaign.objects.get_active() + # skip "i confirm" nag heuristically for all who can multi-request + # TODO: find better heuristic for "trusted users"? + confirm_required = not is_multi + + campaigns = Campaign.objects.get_active() kwargs.update( { "publicbodies": publicbodies, "publicbodies_json": publicbodies_json, "multi_request": is_multi, - "beta_ui": self.request.GET.get("beta") is not None, + "confirm_required": confirm_required, "config": config, "campaigns": campaigns, "js_config": json.dumps(self.get_js_context()), diff --git a/froide/foirequest/views/message.py b/froide/foirequest/views/message.py index 06bc9ea7f..3717cdae5 100644 --- a/froide/foirequest/views/message.py +++ b/froide/foirequest/views/message.py @@ -354,6 +354,7 @@ def edit_postal_message(request, foirequest, message_id): "letterUploadOrScan": _("Upload or scan letter"), "messageReceivedLetter": _("I have received the letter"), "messageSentLetter": _("I have sent the letter"), + "upload": _("Hochladen"), "enterInformation": _("Enter information"), "preview": _("Preview"), "hint": _("Hint"), @@ -486,6 +487,7 @@ def edit_postal_message(request, foirequest, message_id): current="${current}", total="${total}" ), "backToSubmit": _("Back to submit"), + "searchText": pgettext("Search input", "Text"), }, "url": { "tusEndpoint": reverse("api:upload-list"), diff --git a/froide/helper/form_utils.py b/froide/helper/form_utils.py index 56d2d9a6b..966d7b4db 100644 --- a/froide/helper/form_utils.py +++ b/froide/helper/form_utils.py @@ -86,4 +86,6 @@ def field_to_dict(self, name, field): "placeholder": str(field.widget.attrs.get("placeholder", "")), "value": self[name].value() if self.is_bound else None, "choices": choices, + "min_length": getattr(field, "min_length", None), + "max_length": getattr(field, "max_length", None), } diff --git a/froide/local_settings.py.example b/froide/local_settings.py.example index 1613ae0ad..ce11c26fc 100644 --- a/froide/local_settings.py.example +++ b/froide/local_settings.py.example @@ -241,6 +241,7 @@ class Dev(Base): # FROIDE_CONFIG.update(dict( # user_can_hide_web=True, + # user_can_claim_vip=True, # public_body_officials_public=True, # public_body_officials_email_public=False, # request_public_after_due_days=14, diff --git a/froide/locale/de/LC_MESSAGES/django.po b/froide/locale/de/LC_MESSAGES/django.po index 56a8a6531..e3cd8390f 100644 --- a/froide/locale/de/LC_MESSAGES/django.po +++ b/froide/locale/de/LC_MESSAGES/django.po @@ -464,6 +464,34 @@ msgstr "Bilddimensionen zu klein." msgid "Image dimensions are too large." msgstr "Bilddimensionen zu groß." +#: froide/account/forms.py +msgid "My name may appear on the website in plain text" +msgstr "Mein Name soll im Klartext auf dieser Webseite angezeigt werden" + +#: froide/account/forms.py +msgid "My name must be redacted" +msgstr "Mein Name soll geschwärzt werden" + +#: froide/account/forms.py +#, python-brace-format +msgid "{site_name} for journalists" +msgstr "{site_name} für Journalist*innen" + +#: froide/account/forms.py +#, python-brace-format +msgid "You work in journalism and would like to use {site_name} for your research? Shortly after your sign-up is completed, we will send you additional information about extra functionality for journalists." +msgstr "Sind Sie im Journalismus tätig und möchten {site_name} für Ihre Recherchen nutzen? Dann schicken wir Ihnen nach Ihrer Anmeldung Infos über zusätzliche Frunktionen für Journalist*innen." + +#: froide/account/forms.py +#, python-format +msgid "No, I am not a journalist" +msgstr "Nein, ich bin kein*e Journalist*in" + +#: froide/account/forms.py +#, python-format +msgid "Yes, I am a journalist" +msgstr "Ja, ich bin Journalist*in" + #: froide/account/models.py msgid "user tag" msgstr "Nutzer:innen-Badge" @@ -3274,6 +3302,14 @@ msgstr "" "Die erste Nachricht konnte nicht automatisch geschwärzt werden. Bitte " "überprüfen Sie die Nachricht manuell." +#: froide/foirequest/forms/request.py +msgid "I want the request to be immediately accessible to the public on this website. (Default)" +msgstr "Die Anfrage soll sofort öffentlich auf dieser Website erscheinen. (Standardeinstellung)" + +#: froide/foirequest/forms/request.py +msgid "I want the request to remain not public for now, and publish it later." +msgstr "Die Anfrage soll vorerst nicht öffentlich auf dieser Website erscheinen, sondern erst später veröffentlicht werden." + #: froide/foirequest/models/attachment.py msgid "Belongs to message" msgstr "Gehört zu Nachricht" @@ -6573,6 +6609,30 @@ msgstr "" "Bitte wählen Sie zuerst eine Behörde aus und klicken Sie dann auf den " "„Anfrage stellen“-Knopf." +#: froide/foirequest/templates/foirequest/request.html +#, python-format +msgid "Participate in %(name)s" +msgstr "Mitmachen bei %(name)s" + +#: froide/foirequest/templates/foirequest/request.html +#, python-format +msgid "

Your request is accesible for everybody on this website. With your request, you contribute to a public archive of official information. You are part of a growing community that strives for transparency in politics and adminstration.

You can opt into publishing your request later, e.g. after the disclosure of your investigation or research.

" +msgstr "

Ihre Anfrage ist für alle auf dieser Website sichtbar. Denn Sie tragen mit Ihrer Anfrage zu einem öffentlichen Archiv amtlicher Informationen bei und sorgen so mit einer wachsenden Community für Transparenz in Politik und Verwaltung.

Auf Wunsch können Sie Ihre Anfrage aber erst später öffentlich machen, z.B. nach Veröffentlichung Ihrer investigativen Recherche.

" + +#: froide/foirequest/templates/foirequest/request.html +msgid "Your decision here does not affect whether your name will appear in plain text or redacted (as you have decided previously)." +msgstr "Ihre Entscheidung hier hat keinen Einfluss darauf ob ihr Name im Klartext erscheinen soll (wie zuvor festgelegt)." + +#: froide/foirequest/templates/foirequest/request.html +#, python-format +msgid "In your profile settings you can decide whether to your name should be redacted." +msgstr "Ob Ihr Name im Klartext erscheinen sollen, können Sie in ihrem Profil anpassen." + +#: froide/foirequest/views/make_request.py +msgctxt "Make request" +msgid "Log in" +msgstr "Zum Login" + #: froide/foirequest/templates/foirequest/sent.html msgid "Your request has been sent" msgstr "Ihre Anfrage wurde gesendet!" @@ -6904,6 +6964,10 @@ msgstr "" msgid "I already told them" msgstr "Mitteilung ist schon erfolgt" +#: froide/foirequest/templates/foirequest/snippets/request_user_confirm.html +msgid "I confirm that I am not requesting personal information about myself." +msgstr "Ich versichere, dass ich keine persönlichen Informationen über mich selbst anfrage." + #: froide/foirequest/templates/foirequest/upload_postal_message.html #: froide/foirequest/templates/foirequest/upload_postal_message_new.html #, python-format @@ -7597,6 +7661,54 @@ msgstr "" msgid "Similar requests" msgstr "Ähnliche Anfragen" +#: froide/foirequest/views/make_request.py +msgid "Find similar requests" +msgstr "Ähnliche Anfragen finden" + +#: froide/foirequest/views/make_request.py +msgid "Currently chosen:" +msgstr "Aktuell ausgewählt:" + +#: froide/foirequest/views/make_request.py +msgid "Public visibility of the request" +msgstr "Sichtbarkeit der Anfrage" + +#: froide/foirequest/views/make_request.py +msgid "Preview & submit" +msgstr "Vorschau & abschicken" + +#: froide/foirequest/views/make_request.py +msgid "Correct" +msgstr "Korrigieren" + +#: froide/foirequest/views/make_request.py +msgid "Request body" +msgstr "Anfragentext" + +#: froide/foirequest/views/make_request.py +msgid "Not confirmed" +msgstr "Nicht bestätigt" + +#: froide/foirequest/views/make_request.py +msgid "Not agreed" +msgstr "Nicht zugestimmt" + +#: froide/foirequest/views/make_request.py +msgid "Publish request now" +msgstr "Anfrage sofort veröffentlichen" + +#: froide/foirequest/views/make_request.py +msgid "Publish request later" +msgstr "Anfrage zunächst nicht veröffentlichen" + +#: froide/foirequest/views/make_request.py +msgid "Privacy" +msgstr "Privatsphäre" + +#: froide/foirequest/views/make_request.py +msgid "(not sent to public body)" +msgstr "(wird Behörde nicht mitgeteilt)" + #: froide/foirequest/views/make_request.py msgid "Search for more similar requests" msgstr "Suchen Sie nach weiteren ähnlichen Anfragen" @@ -7690,6 +7802,129 @@ msgstr "" msgid "Please confirm your form submission." msgstr "Bitte bestätigen Sie das Absenden der Anfrage." +#: froide/foirequest/views/make_request.py +msgctxt "Publicbody filter" +msgid "Level" +msgstr "Ebene" + +#: froide/foirequest/views/make_request.py +msgctxt "Publicbody filter" +msgid "Location" +msgstr "Standort" + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "You need to create an account at {site_name} so you will be able to manage your request." +msgstr "Damit Sie Ihre Anfrage nach dem Abschicken weiter verwalten können, ist es nötig, einen Account auf {site_name} anzulegen." + +#: froide/foirequest/views/make_request.py +msgid "Do you already have an account?" +msgstr "Sie haben schon einen Account?" + +#: froide/foirequest/views/make_request.py +msgid "Skip this part next time and start at “Choose public body” (optional)" +msgstr "Diesen Teil beim nächsten Mal überspringen und direkt mit „Behörde wählen“ beginnen (optional)" + +#: froide/foirequest/views/make_request.py +msgid "Fix placeholders automatically" +msgstr "Platzhalter automatisch reparieren" + +#: froide/foirequest/views/make_request.py +msgid "Update my postal address" +msgstr "Meine Postadresse aktualisieren" + +#: froide/foirequest/views/make_request.py +msgid "Please write a meaningful subject" +msgstr "Bitte schreiben sie einen relevanten Betreff" + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "The body contains an illegal placeholder ({placeholder})." +msgstr "Der Text enthält den unzulässigen Platzhalter ({placeholder})." + +#: froide/foirequest/views/make_request.py +msgid "None chosen" +msgstr "Keine ausgewählt" + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "Ensure this value has at least {count} character." +msgstr "Bitte sicherstellen, dass der Wert aus mindestens {count} Zeichen besteht." + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "Ensure this value has at least {count} characters." +msgstr "Bitte sicherstellen, dass der Wert aus mindestens {count} Zeichen besteht." + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "Ensure this value has at most {count} character." +msgstr "Bitte sicherstellen, dass der Wert aus höchstens {count} Zeichen besteht." + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "Ensure this value has at most {count} characters." +msgstr "Bitte sicherstellen, dass der Wert aus höchstens {count} Zeichen besteht." + +#: froide/foirequest/views/make_request.py +#, python-brace-format +msgid "{count} results" +msgstr "{count} Ergebnisse" + +#: froide/foirequest/views/make_request.py +msgid "Select this public body" +msgstr "Auch an diese Behörde schreiben" + +#: froide/foirequest/views/make_request.py +msgid "Date range" +msgstr "Zeitraum" + +#: froide/foirequest/views/make_request.py +msgid "Create account" +msgstr "Account anlegen" + +#: froide/foirequest/views/make_request.py +msgid "

Your name will be sent to public bodies. If you want to remain anonymous to the public, you can choose to not display your name on the website. Your name will then be redacted.

" +msgstr "

Ihr Name wird als Teil Ihrer Anfrage an die Behörde gesendet. Falls Sie wünschen, dass Ihr Name nicht auf der Website veröffentlicht wrid, können Sie – nach außen hin – anonym bleiben. Ihr Name wrid dann automatisch geschwärzt.

" + +#: froide/foirequest/views/make_request.py +msgid "

Before you write your own request, you can search our archives. Maybe the information you are looking for has already been published.

" +msgstr "

Suchen Sie in unserem Archiv nach ähnlichen Anfragen, bevor Sie eine eigene Anfrage stellen. Vielleicht sind die gewünschten Informationen bereits veröffentlicht.

" + +#: froide/foirequest/views/make_request.py +msgid "More information about privacy on this website" +msgstr "Weitere Infos zur Privatsphäre auf dieser Website" + +#: froide/foirequest/views/make_request.py +msgid "This form remembers your inputs, as long as stay in the same tab." +msgstr "Dieses Formular speichert ihre Eingaben, so lange Sie das Tab nicht schließen." + +#: froide/foirequest/views/make_request.py +msgid "Continue with selected" +msgstr "Weiter mit Auswahl" + +#: froide/foirequest/views/make_request.py +msgid "Introduction" +msgstr "Einführung" + +#: froide/foirequest/views/make_request.py +msgid "Toggle collapse" +msgstr "Ein-/Ausklappen" + +#: froide/foirequest/views/message.py +#: froide/foirequest/views/make_request.py +msgctxt "Search input" +msgid "Text" +msgstr "Freitext" + +#: froide/foirequest/views/make_request.py +msgid "Skip" +msgstr "Überspringen" + +#: froide/foirequest/views/make_request.py +msgid "What do you want to do?" +msgstr "Was möchten Sie tun?" + #: froide/foirequest/views/message.py msgid "Your message has been sent." msgstr "Ihre Nachricht wurde gesendet." diff --git a/froide/publicbody/api_views.py b/froide/publicbody/api_views.py index 6a5206c70..201c45261 100644 --- a/froide/publicbody/api_views.py +++ b/froide/publicbody/api_views.py @@ -42,7 +42,7 @@ def search_filter(self, queryset, name, value): class JurisdictionViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = JurisdictionSerializer - queryset = Jurisdiction.objects.all() + queryset = Jurisdiction.objects.all().prefetch_related("region") class FoiLawFilter(filters.FilterSet): diff --git a/froide/publicbody/serializers.py b/froide/publicbody/serializers.py index d2edc36f7..6556a5298 100644 --- a/froide/publicbody/serializers.py +++ b/froide/publicbody/serializers.py @@ -30,6 +30,8 @@ class JurisdictionSerializer(serializers.HyperlinkedModelSerializer): region = serializers.HyperlinkedRelatedField( view_name="api:georegion-detail", lookup_field="pk", read_only=True ) + region_kind = serializers.SerializerMethodField() + region_kind_detail = serializers.SerializerMethodField() site_url = serializers.CharField(source="get_absolute_domain_url") class Meta: @@ -44,9 +46,21 @@ class Meta: "slug", "site_url", "region", + "region_kind", + "region_kind_detail", "last_modified_at", ) + def get_region_kind(self, obj): + if obj.region is not None: + return obj.region.kind + return None + + def get_region_kind_detail(self, obj): + if obj.region is not None: + return obj.region.kind_detail + return None + class SimpleFoiLawSerializer(serializers.HyperlinkedModelSerializer): resource_uri = serializers.HyperlinkedIdentityField( diff --git a/froide/settings.py b/froide/settings.py index f175bcfd8..0b6224674 100644 --- a/froide/settings.py +++ b/froide/settings.py @@ -542,6 +542,7 @@ def is_pkce_required(client_id): FROIDE_CONFIG = { "spam_protection": True, "user_can_hide_web": True, + "user_can_claim_vip": False, "public_body_officials_public": True, "public_body_officials_email_public": False, "request_public_after_due_days": 14, diff --git a/froide/tests/live/test_request.py b/froide/tests/live/test_request.py index 7e77650a3..529781405 100644 --- a/froide/tests/live/test_request.py +++ b/froide/tests/live/test_request.py @@ -50,26 +50,43 @@ async def do_login(page, live_server, navigate=True): async def test_make_not_logged_in_request(page, live_server, public_body_with_index): pb = PublicBody.objects.all().first() await go_to_make_request_url(page, live_server) + await page.locator("request-page .btn-primary >> nth=0").click() await page.locator(".search-public_bodies").fill(pb.name) await page.locator(".search-public_bodies-submit").click() buttons = page.locator(".search-results .search-result .btn") await expect(buttons).to_have_count(2) await page.locator(".search-results .search-result .btn >> nth=0").click() - req_title = "FoiRequest Number" - await page.fill("[name=subject]", req_title) - await page.fill("[name=body]", "Documents describing something...") + await page.locator("#step_login_create .btn-primary >> nth=0").click() + await page.fill("[name=first_name]", "Peter") await page.fill("[name=last_name]", "Parker") + await page.fill("[name=address]", "123 Queens Blvd\n12345 Queens") + user_email = "peter.parker@example.com" await page.fill("[name=user_email]", user_email) await page.locator("[name=terms]").click() - await page.locator("#review-button").click() + await page.locator("#step_create_account .btn-primary").click() + + req_title = "FoiRequest Number" + await page.fill("[name=subject]", req_title) + await page.fill("[name=body]", "Documents describing something...") + await page.locator("[name=confirm]").click() + await page.locator("#step_write_request .btn-primary").click() + + await page.locator("#step_request_public .btn-primary").click() - mail.outbox = [] await page.locator("#send-request-button").click() new_account_url = reverse("account-new") + + # FIXME + await page.wait_for_timeout(1000) + # none of these worked, unexpectedly: + # await page.wait_for_url('*'+new_account-url+'*') + # await page.wait_for_url('*') + # await page.wait_for_load_state() + assert new_account_url in page.url new_user = User.objects.get(email=user_email) @@ -99,20 +116,30 @@ async def test_make_not_logged_in_request_to_public_body(page, live_server, worl assert pb await go_to_make_request_url(page, live_server, pb=pb) - user_first_name = "Peter" - user_last_name = "Parker" req_title = "FoiRequest Number" await page.fill("[name=subject]", req_title) await page.fill("[name=body]", "Documents describing something...") + await page.locator("[name=confirm]").click() + await page.locator("#step_write_request .btn-primary").click() + + await page.locator("#step_request_public .btn-primary").click() + + await page.locator("#step_login_create .btn-primary >> nth=0").click() + + user_first_name = "Peter" + user_last_name = "Parker" + user_email = "peter.parker@example.com" await page.fill("[name=first_name]", user_first_name) await page.fill("[name=last_name]", user_last_name) - user_email = "peter.parker@example.com" + await page.fill("[name=address]", "123 Queens Blvd\n12345 Queens") await page.fill("[name=user_email]", user_email) await page.locator("[name=terms]").click() - await page.locator("#review-button").click() + await page.locator("#step_create_account .btn-primary").click() + await page.locator("#send-request-button").click() new_account_url = reverse("account-new") + await page.wait_for_timeout(1000) assert new_account_url in page.url new_user = User.objects.get(email=user_email) assert new_user.first_name == user_first_name @@ -133,6 +160,7 @@ async def test_make_logged_in_request( await do_login(page, live_server) assert dummy_user.is_authenticated await go_to_make_request_url(page, live_server) + await page.locator("request-page .btn-primary >> nth=0").click() pb = PublicBody.objects.all().first() await page.locator(".search-public_bodies").fill(pb.name) await page.locator(".search-public_bodies-submit").click() @@ -144,9 +172,14 @@ async def test_make_logged_in_request( body_text = "Documents describing & something..." await page.fill("[name=subject]", req_title) await page.fill("[name=body]", body_text) - await page.locator("#review-button").click() + await page.locator("[name=confirm]").click() + await page.locator("#step_write_request .btn-primary").click() + + await page.locator("#step_request_public .btn-primary").click() + await page.locator("#send-request-button").click() request_sent = reverse("foirequest-request_sent") + await page.wait_for_timeout(1000) assert request_sent in page.url req = FoiRequest.objects.filter(user=dummy_user).order_by("-id")[0] assert req.title == req_title @@ -177,13 +210,18 @@ async def test_make_logged_in_request_too_many( await do_login(page, live_server) pb = PublicBody.objects.all().first() await go_to_make_request_url(page, live_server, pb=pb) + req_title = "FoiRequest Number" body_text = "Documents describing & something..." await page.fill("[name=subject]", req_title) await page.fill("[name=body]", body_text) - await page.locator("#review-button").click() + await page.locator("[name=confirm]").click() + await page.locator("#step_write_request .btn-primary").click() + + await page.locator("#step_request_public .btn-primary").click() await page.locator("#send-request-button").click() make_request = reverse("foirequest-make_request") + await page.wait_for_timeout(1000) assert make_request in page.url alert = page.locator(".alert-danger", has_text="exceeded your request limit") await expect(alert).to_be_visible() @@ -201,19 +239,27 @@ async def test_make_request_logged_out_with_existing_account(page, live_server, user_last_name = user.last_name await page.fill("[name=subject]", req_title) await page.fill("[name=body]", body_text) + await page.locator("[name=confirm]").click() + await page.locator("#step_write_request .btn-primary").click() + + await page.locator("#id_public_choice1").click() # "not public" + await page.locator("#step_request_public .btn-primary").click() + + await page.locator("#step_login_create .btn-primary >> nth=0").click() + await page.fill("[name=first_name]", user_first_name) await page.fill("[name=last_name]", user_last_name) + await page.fill("[name=address]", "123 Queens Blvd\n12345 Queens") await page.fill("[name=user_email]", user.email) await page.locator("[name=terms]").click() - await page.locator("[name=public]").click() - await page.locator("[name=private]").click() - await page.locator("#review-button").click() + await page.locator("#step_create_account .btn-primary").click() old_count = FoiRequest.objects.filter(user=user).count() draft_count = RequestDraft.objects.filter(user=None).count() await page.locator("#send-request-button").click() new_account_url = reverse("account-new") + await page.wait_for_timeout(1000) assert new_account_url in page.url new_count = FoiRequest.objects.filter(user=user).count() diff --git a/frontend/javascript/components/bs-toast.vue b/frontend/javascript/components/bs-toast.vue index 2007c8400..1f14f2009 100644 --- a/frontend/javascript/components/bs-toast.vue +++ b/frontend/javascript/components/bs-toast.vue @@ -11,6 +11,8 @@ const props = defineProps({ } }) +const emit = defineEmits('dismiss') + let teleportTo = document.getElementById('toastContainer') if (!teleportTo) { @@ -28,7 +30,7 @@ if (!teleportTo) {
- +
diff --git a/frontend/javascript/components/makerequest/intro-campaigns.vue b/frontend/javascript/components/makerequest/intro-campaigns.vue new file mode 100644 index 000000000..8fa942f86 --- /dev/null +++ b/frontend/javascript/components/makerequest/intro-campaigns.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/frontend/javascript/components/makerequest/intro-skip-preference.vue b/frontend/javascript/components/makerequest/intro-skip-preference.vue new file mode 100644 index 000000000..5dab9f0fd --- /dev/null +++ b/frontend/javascript/components/makerequest/intro-skip-preference.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/frontend/javascript/components/makerequest/lib/letter-mixin.js b/frontend/javascript/components/makerequest/lib/letter-mixin.js index 72964e135..ed3bd2658 100644 --- a/frontend/javascript/components/makerequest/lib/letter-mixin.js +++ b/frontend/javascript/components/makerequest/lib/letter-mixin.js @@ -31,12 +31,14 @@ const LetterMixin = { } return `${this.defaultLaw.letter_end}` }, + /* letterSignature() { if (!this.user || (!this.user.first_name && !this.user.last_name)) { return this.i18n.giveName } return false } + */ } } diff --git a/frontend/javascript/components/makerequest/request-form.vue b/frontend/javascript/components/makerequest/request-form.vue index d661ad249..0d428cf06 100644 --- a/frontend/javascript/components/makerequest/request-form.vue +++ b/frontend/javascript/components/makerequest/request-form.vue @@ -2,76 +2,6 @@
-
-
-

- - - - - {{ i18n.change }} - - -

-
-
-
-

{{ i18n._('toMultiPublicBodies', { count: publicBodies.length }) }}

-
-
    -
  • - {{ pb.name }} -
  • -
-
- {{ i18n.batchRequestDraftOnly }} -
- -
-
-
-

- {{ i18n._('toPublicBody', { name: publicBody.name }) }} - - - - - - {{ i18n.change }} - - -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ +

{{ i18n.toPb }} +
+ {{ i18n.none }} + {{ publicBodies.map(pb => pb.name).join(', ') }} + +
+ {{ subject }}
@@ -155,15 +116,6 @@
-
-
    -
  • - {{ error }} -
  • -
-
+
+
+
+
    +
  • + {{ error }} +
  • +
+
+ +