Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
3656dd1
start new request flow implementation
bikubi Sep 1, 2025
1272376
clean up
bikubi Oct 6, 2025
b6ab3d5
refactor filter badge component
bikubi Oct 6, 2025
2663a54
redo steps logic, support the various hide_ GET parameters
bikubi Oct 8, 2025
21ecbb0
syntax fix, cleanup
bikubi Oct 13, 2025
4836264
improve validation of subject + body
bikubi Oct 15, 2025
445dab4
validation for address
bikubi Oct 15, 2025
fa65371
validate some max_lengths, cleanup+fix steps
bikubi Oct 20, 2025
6ec6a30
refactor UserCreateAccount, validate everything as consistently as po…
bikubi Oct 20, 2025
7a8b4d7
restructure public body search
bikubi Oct 27, 2025
73320f9
add "confirm i do not request private info" checkbox
bikubi Nov 5, 2025
a6cceef
implement similar search, with results and pagination
bikubi Nov 5, 2025
fdf7dad
cleanup
bikubi Nov 5, 2025
1e2fc7f
implement onlinehelp links from django slots
bikubi Nov 5, 2025
dd462b8
separate "intro howto" step with static_alias, "skip" preference
bikubi Nov 5, 2025
006592e
refactor "intro campaigns"
bikubi Nov 5, 2025
10cd9ee
fix i18n prop
bikubi Nov 10, 2025
86ca5f5
cleanup
bikubi Nov 10, 2025
bf02e0e
improve ui
bikubi Nov 12, 2025
4e19f56
add "quick select publicbody" to similar request search
bikubi Nov 12, 2025
558ae47
display project (minimally), format date nicely, cleanup
bikubi Nov 12, 2025
d722943
prevent early annoying invalidation on keyboard navigation
bikubi Nov 12, 2025
01c7b5c
fix dark mode simple stepper
bikubi Nov 12, 2025
1884c41
tweaks + cleanup
bikubi Nov 12, 2025
5894206
use new pagination for publicbody search
bikubi Nov 12, 2025
5add6ec
calculate min year
bikubi Nov 12, 2025
4794226
cleanup
bikubi Nov 12, 2025
bcc7eb1
fix stored confirm handling
bikubi Nov 12, 2025
653213c
fix wrong+confusing button
bikubi Nov 12, 2025
4286406
?hide* param tweaks
bikubi Nov 12, 2025
d8739b9
skip intro howto if static_alias empty
bikubi Nov 12, 2025
539e2b1
cleanup
bikubi Nov 12, 2025
d62900b
fix postupload SimpleStepper steps
bikubi Nov 13, 2025
872e20e
fix law_type, add comments
bikubi Nov 13, 2025
ec167a1
fix "first step", when publicbody is preset
bikubi Nov 13, 2025
4a74a4b
remove multiRequest temp hack
bikubi Nov 13, 2025
8f8363a
cleanup
bikubi Nov 13, 2025
1fab2ea
fix flow bugs
bikubi Nov 17, 2025
d9c7452
fix publicbody by-prop vs. from-storage
bikubi Nov 17, 2025
9f0b970
layout cleanup
bikubi Nov 17, 2025
f530d61
clean up onlinehelp links for DjangoSlots related v-bs directives
bikubi Nov 17, 2025
baf5dd7
use proper search API
bikubi Nov 17, 2025
d52ff97
fix onlinehelp link for cms-sourced djangoslots
bikubi Nov 17, 2025
1467dae
move intro howto static_alias to template for override
bikubi Nov 17, 2025
c867f13
cleanup
bikubi Nov 17, 2025
216cb55
skip intro = jump to select publicbody
bikubi Nov 17, 2025
7da3e95
nicer intro campaigns
bikubi Nov 17, 2025
eb7d351
add another intro template, fix campaign logo layout
bikubi Nov 19, 2025
1cb2625
fix disappearing intro step after (failing/invalid) request submit
bikubi Nov 19, 2025
2fc1411
improve skip intro logic
bikubi Nov 19, 2025
dca14ba
cleanup
bikubi Nov 19, 2025
f13c2a0
implement "am journo, claim vip/plus" checkbox
bikubi Nov 24, 2025
4507290
clean up online-help links, main campaign card
bikubi Nov 24, 2025
b7930f5
user confirm label overridable with template
bikubi Nov 24, 2025
89d841a
cleanup
bikubi Nov 26, 2025
4758c89
cleanup
bikubi Nov 26, 2025
782e2f6
fix address handling
bikubi Nov 26, 2025
bdfe142
do not equire extra confirm for trusted users
bikubi Nov 26, 2025
db6aa06
purge storage after logout
bikubi Nov 26, 2025
494e903
remove betaUi flag
bikubi Nov 26, 2025
bdb889b
remove unnecessary form json, save 4MB of traffic!
bikubi Nov 26, 2025
bf4b363
cleanup
bikubi Nov 26, 2025
2e95d9b
adapt beforeunload logic
bikubi Nov 26, 2025
6ccc7ad
cleanup
bikubi Nov 26, 2025
ac8b48c
fix TypeChoiceField vs bool handling
bikubi Nov 26, 2025
8c7a01c
revive regexy text body needles
bikubi Dec 1, 2025
6a6e27a
cleanup
bikubi Dec 1, 2025
aa8e825
add properties to request search api
bikubi Dec 3, 2025
b7fa7ca
i18n + l10n de
bikubi Dec 3, 2025
f4662a2
add missing template
bikubi Dec 10, 2025
66252c9
fix first year calculation
bikubi Dec 10, 2025
61687cd
code readability
bikubi Dec 10, 2025
1a143ac
improve similar search filter badges
bikubi Dec 10, 2025
ceca508
unify filter badge layouts / fix regression
bikubi Dec 11, 2025
127b045
publicbody filter dropdowns scrollable
bikubi Dec 11, 2025
db647f7
fixup badges again
bikubi Dec 11, 2025
7374f48
cleanup
bikubi Dec 11, 2025
9f0ce0d
fix missing i18n
bikubi Dec 11, 2025
49fb70b
keep campaign logo storage as-is
bikubi Dec 11, 2025
f03f29b
✨ add `jurisdiction_rank` parameter to foirequest search endpoint
krmax44 Dec 11, 2025
c6bcf35
♻️ cleanup, add comments
krmax44 Dec 11, 2025
2667bea
🗃️ add campaign logo migration
krmax44 Dec 11, 2025
99daf4a
replace broken mediaqueries, fix layout
bikubi Dec 11, 2025
55b41d6
improve filter dropdowns
bikubi Dec 11, 2025
2900e89
keep dash in time range years centered
bikubi Dec 11, 2025
c542c41
use jurisdiction_rank parameter in frontend
bikubi Dec 11, 2025
248ff4b
hide/disable UserConfirm if slot/template/alias is empty
bikubi Dec 12, 2025
e500d77
♻️ filter by jurisdiction rank without modifying es documents
krmax44 Dec 15, 2025
66a0127
📝 fix typing, add comment
krmax44 Dec 15, 2025
28cace0
♻️ make `RequestPageTourForm` generic
krmax44 Dec 15, 2025
4175e86
fix (non-live) tests
bikubi Dec 15, 2025
20fed28
improve first step for /make-request/to/foo
bikubi Dec 17, 2025
ab5a10f
fix empty address regex handling
bikubi Dec 17, 2025
946700b
fix live tests
bikubi Dec 17, 2025
9b8a9f3
fix initial step
bikubi Dec 19, 2025
d538ee1
split create account in two steps
bikubi Dec 19, 2025
98552c8
improve search similar step ux
bikubi Dec 19, 2025
feb7032
fix live tests, add ids
bikubi Dec 23, 2025
ec459b7
cleanup
bikubi Jan 5, 2026
77be7e1
refine skip intro preference, lower threshold
bikubi Jan 5, 2026
e28f40d
emphasize form-checks
bikubi Jan 5, 2026
d2bbef4
cleanup
bikubi Jan 5, 2026
a8f94ac
ajaxify form submit
bikubi Jan 5, 2026
777190f
fix tests, hackily
bikubi Jan 5, 2026
5852ca9
💄 remove empty breadcrumb item / arrow
krmax44 Jan 8, 2026
36e6fd1
fix form layout, overlong form-check line
bikubi Jan 8, 2026
238077d
streamline multirequest publicbody chooser layout
bikubi Jan 8, 2026
bf2ae10
fix intro layout, mid mobile viewport
bikubi Jan 8, 2026
d13fee7
fix userConfirm validation when static_alias empty
bikubi Jan 8, 2026
5eb7395
fix user address regex check
bikubi Jan 8, 2026
b783135
demote address regex to nag only
bikubi Jan 8, 2026
13431fd
⚡️ fix n+1 query in jurisdiction api
krmax44 Jan 12, 2026
be6ee62
do not purgestorage after sent request
bikubi Jan 12, 2026
d0d5c7c
l10n de
bikubi Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions froide/account/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
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
from froide.helper.form_utils import JSONMixin
from froide.helper.spam import SpamProtectionMixin
from froide.helper.widgets import (
BootstrapCheckboxInput,
BootstrapRadioSelect,
BootstrapSelect,
ImageFileInput,
)
Expand All @@ -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)


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 <strong>not a journalist</strong>")),
(True, _("Yes, I am <strong>a journalist</strong>")),
],
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 <strong>plain text</strong>"
),
),
(True, _("My name must be <strong>redacted</strong>")),
],
coerce=lambda x: x != "False",
)

field_order = ["first_name", "last_name", "user_email"]
Expand Down
1 change: 1 addition & 0 deletions froide/account/templates/account/new.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends 'account/base.html' %}
{% load i18n %}
{% load static %}
{% load frontendbuild %}
{% block title %}
{% trans "New account" %}
{% endblock %}
Expand Down
8 changes: 7 additions & 1 deletion froide/account/tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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(),
}
Expand Down
5 changes: 4 additions & 1 deletion froide/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")


Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
9 changes: 9 additions & 0 deletions froide/campaign/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion froide/foirequest/api_views/request.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
)
Expand Down
21 changes: 21 additions & 0 deletions froide/foirequest/filters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from functools import cache

from django import forms
from django.utils import timezone
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)")),
Expand All @@ -253,6 +268,7 @@ class Meta:
"q",
"status",
"jurisdiction",
"jurisdiction_rank",
"campaign",
"category",
"classification",
Expand All @@ -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(
Expand Down
12 changes: 8 additions & 4 deletions froide/foirequest/forms/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from froide.account.preferences import PreferenceForm, registry


class RequestPageTourForm(PreferenceForm):
class BooleanPreferenceForm(PreferenceForm):
value = forms.TypedChoiceField(
widget=forms.HiddenInput,
choices=(
Expand All @@ -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
)
27 changes: 20 additions & 7 deletions froide/foirequest/forms/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>public</strong> on this website. (Default)"
),
),
(
False,
_(
"I want the request to remain <strong>not public</strong> 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)
Expand Down
18 changes: 16 additions & 2 deletions froide/foirequest/models/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading
Loading