Skip to content

Commit e8362f6

Browse files
apps/projects: add per-project guest users permissions
1 parent 9e12290 commit e8362f6

13 files changed

Lines changed: 232 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ This project (not yet) adheres to [Semantic Versioning](https://semver.org/spec/
77

88
### Unreleased
99

10+
- projects: add per-project `allow_guest_users` setting to control guest user
11+
participation (requires `A4_ENABLE_GUEST_USERS` and django-guest-user).
12+
1013
## meinBerlin-v2606.3
1114

1215
### Added

adhocracy4/dashboard/forms.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22

33
from django import forms
4+
from django.conf import settings
45
from django.contrib.auth import get_user_model
56
from django.forms import RadioSelect
67
from django.forms import inlineformset_factory
@@ -22,6 +23,28 @@
2223
User = get_user_model()
2324

2425

26+
def _coerce_bool_choice(value):
27+
if isinstance(value, bool):
28+
return value
29+
if value in ("True", "true", "1", 1):
30+
return True
31+
if value in ("False", "false", "0", 0):
32+
return False
33+
raise ValueError(f"Invalid boolean choice: {value!r}")
34+
35+
36+
ALLOW_GUEST_USERS_CHOICES = (
37+
(
38+
False,
39+
_("Only registered users can participate"),
40+
),
41+
(
42+
True,
43+
_("Registered and guest users can participate"),
44+
),
45+
)
46+
47+
2548
class ProjectCreateForm(forms.ModelForm):
2649
class Meta:
2750
model = project_models.Project
@@ -71,9 +94,10 @@ class Meta:
7194
"tile_image_alt_text",
7295
"tile_image_copyright",
7396
"is_archived",
97+
"allow_guest_users",
7498
"access",
7599
]
76-
required_for_project_publish = ["name", "description"]
100+
required_for_project_publish = ["name", "description", "allow_guest_users"]
77101
widgets = {
78102
"access": RadioSelect(
79103
choices=[
@@ -86,6 +110,25 @@ class Meta:
86110
),
87111
}
88112

113+
@classmethod
114+
def get_required_fields(cls):
115+
required = super().get_required_fields()
116+
if not getattr(settings, "A4_ENABLE_GUEST_USERS", False):
117+
return [field for field in required if field != "allow_guest_users"]
118+
return required
119+
120+
def __init__(self, *args, **kwargs):
121+
super().__init__(*args, **kwargs)
122+
if not getattr(settings, "A4_ENABLE_GUEST_USERS", False):
123+
self.fields.pop("allow_guest_users", None)
124+
else:
125+
self.fields["allow_guest_users"] = forms.TypedChoiceField(
126+
label=_("Participants"),
127+
choices=ALLOW_GUEST_USERS_CHOICES,
128+
coerce=_coerce_bool_choice,
129+
widget=RadioSelect(),
130+
)
131+
89132

90133
class ProjectInformationForm(ProjectDashboardForm):
91134
contact_heading = _("Contact for questions")
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{% extends "a4forms/includes/form_field.html" %}
2-
{% load i18n %}
2+
{% load i18n a4dashboard_tags %}
3+
4+
{% block required_indicator %}
5+
{% if field.field.required or field.field.required_for_publish %}<span role="presentation" title="{% translate 'This field is required' %}">*</span>{% endif %}
6+
{% endblock %}
37

48
{% block after_label %}
5-
{% if field.field.required_for_publish and not field.value %}
9+
{% if field.field.required_for_publish and field|publish_value_missing %}
610
<i class="fa fa-exclamation-circle u-danger" title="{% translate 'Missing field for publication' %}" aria-label="{% translate 'Missing field for publication' %}"></i>
711
{% endif %}
812
{% endblock %}

adhocracy4/dashboard/templates/a4dashboard/includes/project_basic_form.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
{% include 'a4dashboard/includes/form_field.html' with field=form.description %}
1010

11+
{% if form.allow_guest_users %}
12+
<section class="u-bottom-divider">
13+
{% include 'a4dashboard/includes/form_field.html' with field=form.allow_guest_users %}
14+
</section>
15+
{% endif %}
16+
1117
{% include 'a4dashboard/includes/form_field.html' with field=form.access %}
1218

1319
{% if project.has_finished %}

adhocracy4/dashboard/templatetags/a4dashboard_tags.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
register = template.Library()
66

77

8+
@register.filter
9+
def publish_value_missing(bound_field):
10+
value = bound_field.value
11+
if value is True or value is False:
12+
return False
13+
return value is None or value == ""
14+
15+
816
@register.simple_tag
917
def get_phase_name(type):
1018
name = phases.content[type].name

adhocracy4/forms/templates/a4forms/includes/form_field.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<div class="form-group">
44
<label for="{{ field.id_for_label }}">
5-
{{ field.label }}{% if field.field.required %}<span role="presentation" title="{% translate 'This field is required' %}">*</span>{% endif %}
5+
{{ field.label }}{% block required_indicator %}{% if field.field.required %}<span role="presentation" title="{% translate 'This field is required' %}">*</span>{% endif %}{% endblock %}
66
{% block after_label %}
77
{% endblock %}
88
</label>

adhocracy4/projects/guest_users.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.apps import apps
2+
3+
4+
def is_guest_user(user) -> bool:
5+
"""Return whether user is a temporary guest (django-guest-user).
6+
7+
If django-guest-user is not installed, always returns False.
8+
"""
9+
if not user.is_authenticated:
10+
return False
11+
if not apps.is_installed("guest_user"):
12+
return False
13+
try:
14+
from guest_user.functions import is_guest_user as _is_guest_user
15+
except ImportError:
16+
return False
17+
return _is_guest_user(user)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
("a4projects", "0053_alter_project_description"),
8+
]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name="project",
13+
name="allow_guest_users",
14+
field=models.BooleanField(
15+
default=True,
16+
help_text=(
17+
"Whether guest users may participate in this project. "
18+
"Only applies when guest users are enabled for the platform."
19+
),
20+
verbose_name="Allow guest users to participate",
21+
),
22+
),
23+
]

adhocracy4/projects/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from adhocracy4.models import base
2222

2323
from .enums import Access
24+
from .guest_users import is_guest_user
2425
from .utils import get_module_clusters
2526
from .utils import get_module_clusters_dict
2627

@@ -293,6 +294,15 @@ class Project(
293294

294295
is_app_accessible = models.BooleanField(default=False)
295296

297+
allow_guest_users = models.BooleanField(
298+
default=True,
299+
verbose_name=_("Allow guest users to participate"),
300+
help_text=_(
301+
"Whether guest users may participate in this project. "
302+
"Only applies when guest users are enabled for the platform."
303+
),
304+
)
305+
296306
objects = ProjectManager()
297307

298308
class Meta:
@@ -328,11 +338,14 @@ def has_member(self, user):
328338
Everybody is member of all public projects and private projects can
329339
be joined as moderator or participant.
330340
"""
331-
return (
341+
is_member = (
332342
(user.is_authenticated and self.is_public)
333343
or (user in self.participants.all())
334344
or (user in self.moderators.all())
335345
)
346+
if is_member and is_guest_user(user) and not self.allow_guest_users:
347+
return False
348+
return is_member
336349

337350
def is_group_member(self, user):
338351
if self.group:

adhocracy4/projects/predicates.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,14 @@ def has_context_started(user, item):
7272
if item:
7373
return has_started(user, item.project)
7474
return False
75+
76+
77+
@rules.predicate
78+
def guest_may_participate(user, project):
79+
if not project:
80+
return True
81+
from .guest_users import is_guest_user
82+
83+
if not is_guest_user(user):
84+
return True
85+
return project.allow_guest_users

0 commit comments

Comments
 (0)