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 %} -
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) {
-
-
-
-
-
- {{ i18n._('toMultiPublicBodies', { count: publicBodies.length }) }}
-
-
-
- {{ i18n.change }}
-
-
-
{{ i18n._('toMultiPublicBodies', { count: publicBodies.length }) }}
-- {{ i18n._('toPublicBody', { name: publicBody.name }) }} - - - - - - {{ i18n.change }} - - -
-
+
+
+
+
+
+ {{ i18n._('toMultiPublicBodies', { count: publicBodies.length }) }}
+
+
{{ i18n._('toMultiPublicBodies', { count: publicBodies.length }) }}
+