22 Commits

Author SHA1 Message Date
151ce0d88e fix: massively reduce number of db queries by caching num_per_sex #27
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-11-03 17:00:16 +01:00
e07e633651 style: explicitly return none 2025-11-03 16:53:59 +01:00
dd3b1fde9d feat: Add logs for checking rescue orgs and remove deprecated exclusion 2025-11-03 16:15:11 +01:00
2ffc9b4ba1 feat: Ad django debug toolbar 2025-11-03 16:14:51 +01:00
22eebd4586 feat: Add simple profiler capability 2025-11-03 16:14:05 +01:00
e589a048d3 feat: Make logs in Admin more usable 2025-11-03 16:11:48 +01:00
392eb5a7a8 feat: Use unified explanation for reason for signup 2025-11-02 08:15:11 +01:00
44fa4d4880 feat: Remove requirement to retype password 2025-11-02 08:14:41 +01:00
9b97cc4cb1 fix: Ensure javascript for login is loaded 2025-11-02 08:14:03 +01:00
656a24ef02 feat: Make settings configurable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-21 03:37:36 +02:00
74643db087 feat: Add nicer display of passkeys based on panels 2025-10-21 03:27:08 +02:00
3a6fd3cee1 feat: Add nicer badge 2025-10-21 03:24:19 +02:00
29e9d1bd8c feat: Don't make input for radio button 2025-10-21 03:24:07 +02:00
3c5ca9ae00 feat: Fix display of 2fa options 2025-10-21 02:12:34 +02:00
3d1ad6112d feat: Add link to 2fa options 2025-10-21 02:12:17 +02:00
b843e67e9b feat: put buttons in group 2025-10-21 01:47:17 +02:00
4cab71e8fb feat: Style allauth templates 2025-10-21 01:28:31 +02:00
969339a95f feat: Use allauth and add passkey support 2025-10-21 00:40:10 +02:00
e06efa1539 feat: limit to 10 2025-10-21 14:47:13 +02:00
2fb6d2782f fix: Align button description with function 2025-10-20 21:56:04 +02:00
f69eccd0e4 feat: add page for updating the exclusion reason where it's not set yet 2025-10-20 18:33:15 +02:00
e20e6d4b1d fix: typo 2025-10-20 18:31:10 +02:00
40 changed files with 608 additions and 173 deletions

View File

@@ -8,6 +8,7 @@ host=localhost
[django] [django]
secret=CHANGE-ME secret=CHANGE-ME
debug=True debug=True
internal_ips=["127.0.0.1"]
[database] [database]
backend=sqlite3 backend=sqlite3
@@ -28,3 +29,6 @@ django_log_level=INFO
api_url=https://photon.hyteck.de/api api_url=https://photon.hyteck.de/api
api_format=photon api_format=photon
[security]
totp_issuer="NF Localhost"
webauth_allow_insecure_origin=True

View File

@@ -38,7 +38,8 @@ dependencies = [
"celery[redis]", "celery[redis]",
"drf-spectacular[sidecar]", "drf-spectacular[sidecar]",
"django-widget-tweaks", "django-widget-tweaks",
"django-super-deduper" "django-super-deduper",
"django-allauth[mfa]",
] ]
dynamic = ["version", "readme"] dynamic = ["version", "readme"]
@@ -48,6 +49,7 @@ develop = [
"pytest", "pytest",
"coverage", "coverage",
"model_bakery", "model_bakery",
"debug_toolbar",
] ]
docs = [ docs = [
"sphinx", "sphinx",

View File

@@ -164,6 +164,13 @@ class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",) list_filter = ("platform",)
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
ordering = ["-created_at"]
list_filter = ("action",)
list_display = ("action", "user", "created_at")
admin.site.register(Animal) admin.site.register(Animal)
admin.site.register(Species) admin.site.register(Species)
admin.site.register(Rule) admin.site.register(Rule)
@@ -172,5 +179,4 @@ admin.site.register(ModerationAction)
admin.site.register(Language) admin.site.register(Language)
admin.site.register(Announcement) admin.site.register(Announcement)
admin.site.register(Subscriptions) admin.site.register(Subscriptions)
admin.site.register(Log)
admin.site.register(Timestamp) admin.site.register(Timestamp)

View File

@@ -1,4 +1,5 @@
from django import forms from django import forms
from django.forms.widgets import Textarea
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \ from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
@@ -9,6 +10,8 @@ from django.utils.translation import gettext_lazy as _
from notfellchen.settings import MEDIA_URL from notfellchen.settings import MEDIA_URL
from crispy_forms.layout import Div from crispy_forms.layout import Div
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text
def animal_validator(value: str): def animal_validator(value: str):
value = value.lower() value = value.lower()
@@ -137,6 +140,18 @@ class ModerationActionForm(forms.ModelForm):
fields = ('action', 'public_comment', 'private_comment') fields = ('action', 'public_comment', 'private_comment')
class AddedRegistrationForm(forms.Form):
reason_for_signup = forms.CharField(label=reason_for_signup_label,
help_text=reason_for_signup_help_text,
widget=Textarea)
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um "
"sicherzustellen, dass du kein Roboter bist."))
def signup(self, request, user):
pass
class CustomRegistrationForm(RegistrationForm): class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta): class Meta(RegistrationForm.Meta):
model = User model = User

View File

@@ -13,7 +13,8 @@ from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position from .tools.geo import LocationProxy, Position
from .tools.misc import time_since_as_hr_string from .tools.misc import time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \ from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices, reason_for_signup_label, \
reason_for_signup_help_text
from .tools.model_helpers import ndm as NotificationDisplayMapping from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -267,10 +268,6 @@ class RescueOrganization(models.Model):
""" """
return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile
def set_exclusion_from_checks(self):
self.exclude_from_check = True
self.save()
@property @property
def child_organizations(self): def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self) return RescueOrganization.objects.filter(parent_org=self)
@@ -311,8 +308,7 @@ class User(AbstractUser):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True, organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation')) verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_( reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True) email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
REQUIRED_FIELDS = ["reason_for_signup", "email"] REQUIRED_FIELDS = ["reason_for_signup", "email"]
@@ -423,9 +419,10 @@ class AdoptionNotice(models.Model):
@property @property
def num_per_sex(self): def num_per_sex(self):
print(f"{self.pk} x")
num_per_sex = dict() num_per_sex = dict()
for sex in SexChoices: for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count() num_per_sex[sex] = len([animal for animal in self.animals if animal.sex == sex])
return num_per_sex return num_per_sex
@property @property
@@ -515,6 +512,7 @@ class AdoptionNotice(models.Model):
photos.extend(animal.photos.all()) photos.extend(animal.photos.all())
if len(photos) > 0: if len(photos) > 0:
return photos return photos
return None
def get_photo(self): def get_photo(self):
""" """

View File

@@ -0,0 +1,12 @@
{% load allauth %}
{% setvar variant %}
{% if "primary" in attrs.tags %}
is-success
{% elif "secondary" in attrs.tags %}
is-success is-light
{% endif %}
{% endsetvar %}
<span class="tag{% if variant %} {{ variant }}{% endif %}" {% if attrs.title %}title="{{ attrs.title }}"{% endif %}>
{% slot %}
{% endslot %}
</span>

View File

@@ -0,0 +1,15 @@
{% load allauth %}
{% comment %} djlint:off {% endcomment %}
<div class="control">
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
class="button is-primary"
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.value %}value="{{ attrs.value }}"{% endif %}
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
>
{% slot %}
{% endslot %}
</{% if attrs.href %}a{% else %}button{% endif %}>
</div>

View File

@@ -0,0 +1,5 @@
{% load allauth %}
<div class="field is-grouped">
{% slot %}
{% endslot %}
</div>

View File

@@ -0,0 +1,50 @@
{% load allauth %}
<div class="field">
{% if attrs.type == "textarea" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
<textarea class="textarea"
{% if attrs.required %}required{% endif %}
{% if attrs.rows %}rows="{{ attrs.rows }}"{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}>{% slot value %}{% endslot %}</textarea>
{% else %}
{% if attrs.type != "checkbox" and attrs.type != "radio" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
<input {% if attrs.type != "checkbox" and attrs.type != "radio" %}class="input"{% endif %}
{% if attrs.required %}required{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
{% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
{% if attrs.value is not None %}value="{{ attrs.value }}"{% endif %}
type="{{ attrs.type }}">
{% if attrs.type == "checkbox" or attrs.type == "radio" %}
<label for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
{% endif %}
{% if slots.help_text %}
<p class="help is-danger">
{% slot help_text %}
{% endslot %}
</p>
{% endif %}
<p class="help is-danger">{{ attrs.errors }}</p>
</div>

View File

@@ -0,0 +1 @@
{{ attrs.form }}

View File

@@ -0,0 +1,12 @@
{% load allauth %}
<div class="block">
<form method="{{ attrs.method }}"
{% if attrs.action %}action="{{ attrs.action }}"{% endif %}>
{% slot body %}
{% endslot %}
<div class="field is-grouped is-grouped-multiline">
{% slot actions %}
{% endslot %}
</div>
</form>
</div>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h1 class="title is-1">{% slot %}{% endslot %}</h1>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h2 class="title is-2">{% slot %}{% endslot %}</h2>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<p class="content">{% slot %}{% endslot %}</p>

View File

@@ -0,0 +1,18 @@
{% load allauth %}
<section class="block">
<h2 class="title is-2">
{% slot title %}
{% endslot %}
</h2>
{% slot body %}
{% endslot %}
{% if slots.actions %}
<div class="field is-grouped is-grouped-multiline">
{% for action in slots.actions %}
<div class="control">
{{ action }}
</div>
{% endfor %}
</div>
{% endif %}
</section>

View File

@@ -0,0 +1 @@
{% extends "fellchensammlung/base.html" %}

View File

@@ -37,6 +37,26 @@
{% block header %} {% block header %}
{% include "fellchensammlung/header.html" %} {% include "fellchensammlung/header.html" %}
{% endblock %} {% endblock %}
{% if profile %}
<div class="profile">
<table class="table is-bordered is-fullwidth is-hoverable is-striped">
<thead>
<tr>
<td>Timestamp</td>
<td>Status</td>
</tr>
</thead>
<tbody>
{% for status in profile %}
<tr>
<td>{{ status.0 }}</td>
<td>{{ status.1 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="main-content"> <div class="main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
@@ -45,5 +65,8 @@
{% block footer %} {% block footer %}
{% include "fellchensammlung/footer.html" %} {% include "fellchensammlung/footer.html" %}
{% endblock %} {% endblock %}
{% block extra_body %}
{% endblock extra_body %}
</body> </body>
</html> </html>

View File

@@ -19,53 +19,10 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="block"> <div class="block">
<div class="card"> {% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>
</div> </div>
<div class="block"> <div class="block">
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div> </div>
<div class="block"> <div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}"> <a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">

View File

@@ -1,7 +1,8 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load account %}
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %} {% block title %}<title>{% user_display user %}</title>{% endblock %}
{% block content %} {% block content %}
@@ -13,7 +14,7 @@
</div> </div>
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<form class="" action="{% url 'logout' %}" method="post"> <form class="" action="{% url 'account_logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button" type="submit"> <button class="button" type="submit">
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout <i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
@@ -25,69 +26,87 @@
<div class="block"> <div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2> <h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p> <div class="block"><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</div>
<div class="">
<p>
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
</p>
</div>
</div>
{% if user.id is request.user.id %}
<div class="block"> <div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2> <div class="field is-grouped is-grouped-multiline">
<form class="block" action="" method="POST"> <div class="control">
{% csrf_token %} <a class="button is-warning"
{% if user.email_notifications %} href="{% url 'account_change_password' %}">{% translate "Passwort ändern" %}</a>
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
</form>
<details>
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
<div class="block">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="button is-danger" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="button is-primary" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
</div> </div>
</details> <div class="control">
<a class="button is-warning"
href="{% url 'account_email' %}">
{% translate "E-Mail Adresse ändern" %}
</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'mfa_index' %}">
{% translate "2-Faktor Authentifizierung" %}
</a>
</div>
<div class="control">
<a class="button is-info" href="{% url 'user-me-export' %}">
{% translate "Daten exportieren" %}
</a>
</div>
</div>
</div> </div>
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2> {% if user.id is request.user.id %}
{% include "fellchensammlung/lists/list-notifications.html" %} <div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST">
{% csrf_token %}
{% if user.email_notifications %}
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
</form>
<details>
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
<div class="block">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="button is-danger" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="button is-primary" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
</div>
</details>
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2> </div>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2> <h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %} {% include "fellchensammlung/lists/list-notifications.html" %}
{% endif %} <h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %} {% block title %}<title>{% translate "404 Forbidden" %}</title>{% endblock %}
{% block content %} {% block content %}
<h1 class="title is-1">404 Not Found</h1> <h1 class="title is-1">404 Not Found</h1>

View File

@@ -1,18 +1,35 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load admin_urls %}
{% block title %} {% block title %}
<title>Organisation {{ rescue_org }} von regelmäßiger Prüfung ausschließen</title> <title>Organisation {{ org }} von regelmäßiger Prüfung ausschließen</title>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1 class="title is-1">Organisation {{ rescue_org }} von regelmäßiger Prüfung ausschließen</h1> <h1 class="title is-1">Organisation {{ org }} von regelmäßiger Prüfung ausschließen</h1>
<form method="post"> <div class="columns block">
{% csrf_token %} <div class="column">
{{ form }} {% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
</div>
<div class="column">
{% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div>
</div>
<div class="block">
<form method="post">
{% csrf_token %}
{{ form }}
<a class="button" href="{% url 'organization-check' %}">{% translate "Zurück (nicht exkludieren)" %}</a> <input class="button is-primary" type="submit" name="delete"
<input class="button is-danger" type="submit" name="delete" value="{% translate "Von regelmäßiger Prüfung ausschließen" %}"> value="{% translate "Aktualisieren" %}">
</form> <a class="button" href="{% url 'organization-check' %}">{% translate "Zurück" %}</a>
</form>
</div>
<div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
</div>
{% endblock %} {% endblock %}

View File

@@ -27,7 +27,7 @@
{{ field|add_class:"input" }} {{ field|add_class:"input" }}
{% endif %} {% endif %}
</div> </div>
<div class="help"> <div class="help content">
{{ field.help_text }} {{ field.help_text }}
</div> </div>
<div class="help is-danger"> <div class="help is-danger">

View File

@@ -49,10 +49,10 @@
{% else %} {% else %}
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}"> <a class="button is-primary" href="{% url "account_signup" %}">
<strong>{% translate "Registrieren" %}</strong> <strong>{% translate "Registrieren" %}</strong>
</a> </a>
<a class="button is-light" href="{% url "login" %}"> <a class="button is-light" href="{% url "account_login" %}">
<strong>{% translate "Login" %}</strong> <strong>{% translate "Login" %}</strong>
</a> </a>
</div> </div>

View File

@@ -3,7 +3,7 @@
{% if rescue_organizations %} {% if rescue_organizations %}
{% for rescue_organization in rescue_organizations %} {% for rescue_organization in rescue_organizations %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-rescue-organization.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization.html" %}
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -0,0 +1,46 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>

View File

@@ -1,19 +1,20 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<div class="grid is-col-min-2"> <div class="grid is-col-min-2">
{% if adoption_notice.num_per_sex.F > 0 %} {% with num_per_sex=adoption_notice.num_per_sex %}
<span class="cell icon-text tag is-medium"> {% if num_per_sex.F > 0 %}
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.F }} </span> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ num_per_sex.F }}</span>
<span class="icon"> <span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}" <img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}"
alt="{% translate 'weibliche Tiere' %}"> alt="{% translate 'weibliche Tiere' %}">
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.I > 0 %} {% if num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.I }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.I }}</span>
<span class="icon"> <span class="icon">
<img class="icon" <img class="icon"
@@ -21,24 +22,25 @@
alt="{% translate 'intersexuelle Tiere' %}"> alt="{% translate 'intersexuelle Tiere' %}">
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.M > 0 %} {% if num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.M }}</span>
<span class="icon"> <span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}" <img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}"
alt="{% translate 'männliche Tiere' %}"> alt="{% translate 'männliche Tiere' %}">
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.M_N > 0 %} {% if num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M_N }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.M_N }}</span>
<span class="icon"> <span class="icon">
<img class="icon" <img class="icon"
src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}" src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}"
alt="{% translate 'männlich, kastrierte Tiere' %}"> alt="{% translate 'männlich, kastrierte Tiere' %}">
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% endwith %}
</div> </div>

View File

@@ -38,7 +38,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_to_check %} {% for rescue_org in rescue_orgs_to_check %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -50,7 +50,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_with_ongoing_communication %} {% for rescue_org in rescue_orgs_with_ongoing_communication %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -62,7 +62,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_last_checked %} {% for rescue_org in rescue_orgs_last_checked %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,43 @@
{% extends "mfa/recovery_codes/base.html" %}
{% load i18n %}
{% load allauth %}
{% block content %}
{% element h1 %}
{% translate "Recovery Codes" %}
{% endelement %}
{% element p %}
{% blocktranslate count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }}
recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes
available.{% endblocktranslate %}
{% endelement %}
<div class="block">
{% element field id="recovery_codes" type="textarea" disabled=True rows=unused_codes|length readonly=True %}
{% slot label %}
{% translate "Unused codes" %}
{% endslot %}
{% comment %} djlint:off {% endcomment %}
{% slot value %}{% for code in unused_codes %}{% if forloop.counter0 %}
{% endif %}{{ code }}{% endfor %}{% endslot %}
{% comment %} djlint:on {% endcomment %}
{% endelement %}
</div>
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% if unused_codes %}
{% url 'mfa_download_recovery_codes' as download_url %}
<div class="control">
{% element button href=download_url %}
{% translate "Download codes" %}
{% endelement %}
</div>
{% endif %}
{% url 'mfa_generate_recovery_codes' as generate_url %}
<div class="control">
{% element button href=generate_url %}
{% translate "Generate new codes" %}
{% endelement %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,78 @@
{% extends "mfa/webauthn/base.html" %}
{% load i18n %}
{% load static %}
{% load allauth %}
{% load humanize %}
{% block content %}
{% element h1 %}
{% trans "Security Keys" %}
{% endelement %}
{% if authenticators|length == 0 %}
{% element p %}
{% blocktranslate %}No security keys have been added.{% endblocktranslate %}
{% endelement %}
{% else %}
<article class="panel">
<p class="panel-heading">{% trans "Security Keys" %}</p>
{% for authenticator in authenticators %}
<div class="panel-block">
<div class="level" style="width: 100%;">
<div class="level-left">
<div class="level-item">
{% if authenticator.wrap.is_passwordless is True %}
{% element badge tags="mfa,key,primary" %}
{% translate "Passkey" %}
{% endelement %}
{% elif authenticator.wrap.is_passwordless is False %}
{% element badge tags="mfa,key,secondary" %}
{% translate "Security key" %}
{% endelement %}
{% else %}
{% element badge title=_("This key does not indicate whether it is a passkey.") tags="mfa,key,warning" %}
{% translate "Unspecified" %}
{% endelement %}
{% endif %}
</div>
<div class="level-item">
<strong>
{{ authenticator }}
</strong>
</div>
<div class="level-item">
{% blocktranslate with created_at=authenticator.created_at|date:"SHORT_DATE_FORMAT" %}
Added
on {{ created_at }}{% endblocktranslate %}.
</div>
<div class="level-item">
{% if authenticator.last_used_at %}
{% blocktranslate with last_used=authenticator.last_used_at|naturaltime %}Last used
{{ last_used }}{% endblocktranslate %}
{% else %}
Not used.
{% endif %}
</div>
</div>
<div class="level-right">
<div class="level-item">
{% url 'mfa_edit_webauthn' pk=authenticator.pk as edit_url %}
{% element button tags="mfa,authenticator,edit,tool" href=edit_url %}
{% translate "Edit" %}
{% endelement %}
</div>
<div class="level-item">
{% url 'mfa_remove_webauthn' pk=authenticator.pk as remove_url %}
{% element button tags="mfa,authenticator,danger,delete,tool" href=remove_url %}
{% translate "Remove" %}
{% endelement %}
</div>
</div>
</div>
</div>
{% endfor %}
</article>
{% endif %}
{% url 'mfa_add_webauthn' as add_url %}
{% element button href=add_url %}
{% translate "Add" %}
{% endelement %}
{% endblock %}

View File

@@ -1,5 +1,6 @@
import datetime as datetime import datetime as datetime
import logging import logging
import time
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -75,3 +76,20 @@ def is_404(url):
return result.status_code == 404 return result.status_code == 404
except requests.RequestException as e: except requests.RequestException as e:
logging.warning(f"Request to {url} failed: {e}") logging.warning(f"Request to {url} failed: {e}")
class RequestProfiler:
data = []
def add_status(self, status):
self.data.append((time.time(), status))
@property
def as_relative(self):
first_ts = self.data[0][0]
return [(datum[0] - first_ts, datum[1]) for datum in self.data]
@property
def as_relative_with_ms(self):
first_ts = self.data[0][0]
return [(f"{(datum[0] - first_ts)*1000:.4}ms", datum[1]) for datum in self.data]

View File

@@ -134,3 +134,14 @@ class RegularCheckStatusChoices(models.TextChoices):
EXCLUDED_OTHER_ORG = "excluded_other_org", _("Exkludiert: Andere Organisation wird geprüft") EXCLUDED_OTHER_ORG = "excluded_other_org", _("Exkludiert: Andere Organisation wird geprüft")
EXCLUDED_SCOPE = "excluded_scope", _("Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen") EXCLUDED_SCOPE = "excluded_scope", _("Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen")
EXCLUDED_OTHER = "excluded_other", _("Exkludiert: Anderer Grund") EXCLUDED_OTHER = "excluded_other", _("Exkludiert: Anderer Grund")
##########
## USER ##
##########
reason_for_signup_label = _("Grund für die Registrierung")
reason_for_signup_help_text = _(
"Wir würden gerne wissen warum du dich registrierst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen "
"willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest "
"kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.")

View File

@@ -49,6 +49,8 @@ urlpatterns = [
name="rescue-organization-detail"), name="rescue-organization-detail"),
path("tierschutzorganisationen/<int:rescue_organization_id>/exkludieren", views.exclude_from_regular_check, path("tierschutzorganisationen/<int:rescue_organization_id>/exkludieren", views.exclude_from_regular_check,
name="rescue-organization-exclude"), name="rescue-organization-exclude"),
path("tierschutzorganisationen/add-exclusion-reason", views.update_exclusion_reason,
name="rescue-organization-add-exclusion-reason"),
path("tierschutzorganisationen/spezialisierung/<slug:species_slug>", views.specialized_rescues, path("tierschutzorganisationen/spezialisierung/<slug:species_slug>", views.specialized_rescues,
name="specialized-rescue-organizations"), name="specialized-rescue-organizations"),
@@ -92,15 +94,6 @@ urlpatterns = [
path("user/notifications/", views.my_notifications, name="user-notifications"), path("user/notifications/", views.my_notifications, name="user-notifications"),
path('user/me/export/', views.export_own_profile, name='user-me-export'), path('user/me/export/', views.export_own_profile, name='user-me-export'),
path('accounts/register/',
registration_views.HTMLMailRegistrationView.as_view(
form_class=CustomRegistrationForm,
email_body_template="fellchensammlung/mail/activation_email.html",
),
name='django_registration_register',
),
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('change-language', views.change_language, name="change-language"), path('change-language', views.change_language, name="change-language"),

View File

@@ -1,5 +1,4 @@
import logging import logging
from datetime import timedelta
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -37,6 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti
from .tasks import post_adoption_notice_save from .tasks import post_adoption_notice_save
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from .tools.misc import RequestProfiler
from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates, RegularCheckStatusChoices from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates, RegularCheckStatusChoices
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
@@ -242,11 +242,17 @@ def search_important_locations(request, important_location_slug):
def search(request, templatename="fellchensammlung/search.html"): def search(request, templatename="fellchensammlung/search.html"):
search_profile = RequestProfiler()
search_profile.add_status("Start")
# A user just visiting the search site did not search, only upon completing the search form a user has really # A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button # searched. This will toggle the "subscribe" button
searched = False searched = False
search_profile.add_status("Init Search")
search = AdoptionNoticeSearch() search = AdoptionNoticeSearch()
search_profile.add_status("Search from request starting")
search.adoption_notice_search_from_request(request) search.adoption_notice_search_from_request(request)
search_profile.add_status("Search from request finished")
if request.method == 'POST': if request.method == 'POST':
searched = True searched = True
if "subscribe_to_search" in request.POST: if "subscribe_to_search" in request.POST:
@@ -266,10 +272,12 @@ def search(request, templatename="fellchensammlung/search.html"):
subscribed_search = search.get_subscription_or_none(request.user) subscribed_search = search.get_subscription_or_none(request.user)
else: else:
subscribed_search = None subscribed_search = None
search_profile.add_status("End of POST")
site_title = _("Suche") site_title = _("Suche")
site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe suchen.") site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe suchen.")
canonical_url = reverse("search") canonical_url = reverse("search")
search_profile.add_status("Start of context")
context = {"adoption_notices": search.get_adoption_notices(), context = {"adoption_notices": search.get_adoption_notices(),
"search_form": search.search_form, "search_form": search.search_form,
"place_not_found": search.place_not_found, "place_not_found": search.place_not_found,
@@ -286,6 +294,10 @@ def search(request, templatename="fellchensammlung/search.html"):
"site_title": site_title, "site_title": site_title,
"site_description": site_description, "site_description": site_description,
"canonical_url": canonical_url} "canonical_url": canonical_url}
search_profile.add_status("End of context")
if request.user.is_superuser:
context["profile"] = search_profile.as_relative_with_ms
search_profile.add_status("Finished - returing render")
return render(request, templatename, context=context) return render(request, templatename, context=context)
@@ -578,12 +590,20 @@ def report_detail_success(request, report_id):
def user_detail(request, user, token=None): def user_detail(request, user, token=None):
user_detail_profile = RequestProfiler()
user_detail_profile.add_status("Start")
adoption_notices = AdoptionNotice.objects.filter(owner=user)
user_detail_profile.add_status("Finished fetching adoption notices")
context = {"user": user, context = {"user": user,
"adoption_notices": AdoptionNotice.objects.filter(owner=user), "adoption_notices": adoption_notices,
"notifications": Notification.objects.filter(user_to_notify=user, read=False), "notifications": Notification.objects.filter(user_to_notify=user, read=False),
"search_subscriptions": SearchSubscription.objects.filter(owner=user), } "search_subscriptions": SearchSubscription.objects.filter(owner=user), }
user_detail_profile.add_status("End of context")
if token is not None: if token is not None:
context["token"] = token context["token"] = token
user_detail_profile.add_status("Finished - returning to renderer")
if request.user.is_superuser:
context["profile"] = user_detail_profile.as_relative_with_ms
return render(request, 'fellchensammlung/details/detail-user.html', context=context) return render(request, 'fellchensammlung/details/detail-user.html', context=context)
@@ -654,7 +674,7 @@ def my_notifications(request):
context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by( context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by(
"-created_at"), "-created_at"),
"notifications_read_last": Notification.objects.filter(user_to_notify=request.user, "notifications_read_last": Notification.objects.filter(user_to_notify=request.user,
read=True).order_by("-read_at")} read=True).order_by("-read_at")[:10]}
return render(request, 'fellchensammlung/notifications.html', context=context) return render(request, 'fellchensammlung/notifications.html', context=context)
@@ -846,8 +866,7 @@ def rescue_organization_check(request, context=None):
action = request.POST.get("action") action = request.POST.get("action")
if action == "checked": if action == "checked":
rescue_org.set_checked() rescue_org.set_checked()
elif action == "exclude": Log.objects.create(user=request.user, action="rescue_organization_checked", )
rescue_org.set_exclusion_from_checks()
elif action == "set_species_url": elif action == "set_species_url":
species_url_form = SpeciesURLForm(request.POST) species_url_form = SpeciesURLForm(request.POST)
@@ -858,6 +877,7 @@ def rescue_organization_check(request, context=None):
elif action == "update_internal_comment": elif action == "update_internal_comment":
comment_form = RescueOrgInternalComment(request.POST, instance=rescue_org) comment_form = RescueOrgInternalComment(request.POST, instance=rescue_org)
if comment_form.is_valid(): if comment_form.is_valid():
Log.objects.create(user=request.user, action="rescue_organization_added_internal_comment", )
comment_form.save() comment_form.save()
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False, rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False,
@@ -897,23 +917,42 @@ def rescue_organization_check_dq(request):
return rescue_organization_check(request, context) return rescue_organization_check(request, context)
@user_passes_test(user_is_trust_level_or_above) def exclude_from_regular_check(request, rescue_organization_id, source="organization-check"):
def exclude_from_regular_check(request, rescue_organization_id):
rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id) rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
if request.method == "POST": if request.method == "POST":
form = UpdateRescueOrgRegularCheckStatus(request.POST, instance=rescue_org) form = UpdateRescueOrgRegularCheckStatus(request.POST, instance=rescue_org)
if form.is_valid(): if form.is_valid():
form.save() form.save()
if form.cleaned_data["regular_check_status"] != RegularCheckStatusChoices.REGULAR_CHECK: to_be_excluded = form.cleaned_data["regular_check_status"] != RegularCheckStatusChoices.REGULAR_CHECK
rescue_org.exclude_from_check = True rescue_org.exclude_from_check = to_be_excluded
rescue_org.save() rescue_org.save()
return redirect(reverse("organization-check")) if to_be_excluded:
Log.objects.create(user=request.user,
action="rescue_organization_excluded_from_check",
text=f"New status: {form.cleaned_data["regular_check_status"]}")
return redirect(reverse(source))
else: else:
form = UpdateRescueOrgRegularCheckStatus(instance=rescue_org) form = UpdateRescueOrgRegularCheckStatus(instance=rescue_org)
context = {"form": form, rescue_org: rescue_org} org_meta = rescue_org._meta
context = {"form": form, "org": rescue_org, "org_meta": org_meta}
return render(request, 'fellchensammlung/forms/form-exclude-org-from-check.html', context=context) return render(request, 'fellchensammlung/forms/form-exclude-org-from-check.html', context=context)
@user_passes_test(user_is_trust_level_or_above)
def update_exclusion_reason(request):
"""
This view will redirect to update a rescue org that not yet has an exclusion reason but is excluded
"""
orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=True,
regular_check_status=RegularCheckStatusChoices.REGULAR_CHECK)
if orgs_to_check.count() > 0:
return exclude_from_regular_check(request, orgs_to_check[0].pk,
source="rescue-organization-add-exclusion-reason")
else:
return render(request, "fellchensammlung/errors/404.html", status=404)
@user_passes_test(user_is_trust_level_or_above) @user_passes_test(user_is_trust_level_or_above)
def moderation_tools_overview(request): def moderation_tools_overview(request):
context = None context = None

View File

@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
import json
from pathlib import Path from pathlib import Path
import os import os
import configparser import configparser
@@ -74,6 +74,10 @@ except configparser.NoSectionError:
raise BaseException("No config found or no Django Secret is configured!") raise BaseException("No config found or no Django Secret is configured!")
DEBUG = config.getboolean('django', 'debug', fallback=False) DEBUG = config.getboolean('django', 'debug', fallback=False)
# Internal IPs
raw_config_value = config.get("django", "internal_ips", fallback=[])
INTERNAL_IPS = json.loads(raw_config_value)
""" DATABASE """ """ DATABASE """
DB_BACKEND = config.get("database", "backend", fallback="sqlite3") DB_BACKEND = config.get("database", "backend", fallback="sqlite3")
DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3") DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
@@ -85,6 +89,7 @@ DB_HOST = config.get("database", "host", fallback='')
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')] LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
""" CELERY + KEYDB """ """ CELERY + KEYDB """
CELERY_BROKER_URL = config.get("celery", "broker", fallback="redis://localhost:6379/0") CELERY_BROKER_URL = config.get("celery", "broker", fallback="redis://localhost:6379/0")
CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localhost:6379/0") CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localhost:6379/0")
@@ -130,6 +135,37 @@ ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
REGISTRATION_OPEN = True REGISTRATION_OPEN = True
REGISTRATION_SALT = "notfellchen" REGISTRATION_SALT = "notfellchen"
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by email
'allauth.account.auth_backends.AuthenticationBackend',
]
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_SIGNUP_FIELDS = ['username*', "email*", "password1*"]
ACCOUNT_SIGNUP_FORM_CLASS = 'fellchensammlung.forms.AddedRegistrationForm'
MFA_SUPPORTED_TYPES = ["totp",
"webauthn",
"recovery_codes"]
MFA_TOTP_TOLERANCE = 1
MFA_TOTP_ISSUER = config.get('security', 'totp_issuer', fallback="Notfellchen")
MFA_PASSKEY_LOGIN_ENABLED = True
MFA_PASSKEY_SIGNUP_ENABLED = True
# Optional -- use for local development only: the WebAuthn uses the
#``fido2`` package, and versions up to including version 1.1.3 do not
# regard localhost as a secure origin, which is problematic during
# local development and testing.
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = config.get('security', 'webauth_allow_insecure_origin', fallback=False)
""" SECURITY.TXT """ """ SECURITY.TXT """
SEC_CONTACT = config.get("security", "Contact", fallback="julian-samuel@gebuehr.net") SEC_CONTACT = config.get("security", "Contact", fallback="julian-samuel@gebuehr.net")
SEC_EXPIRES = config.get("security", "Expires", fallback="2028-03-17T07:00:00.000Z") SEC_EXPIRES = config.get("security", "Expires", fallback="2028-03-17T07:00:00.000Z")
@@ -182,7 +218,11 @@ INSTALLED_APPS = [
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
"django.contrib.humanize",
'django.contrib.messages', 'django.contrib.messages',
'allauth',
'allauth.account',
'allauth.mfa',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
"django.contrib.sitemaps", "django.contrib.sitemaps",
'fontawesomefree', 'fontawesomefree',
@@ -193,11 +233,13 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery 'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks' 'widget_tweaks',
"debug_toolbar",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
# Static file serving & caching # Static file serving & caching
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -208,6 +250,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
# allauth middleware, needs to be after message middleware
"allauth.account.middleware.AccountMiddleware",
] ]
ROOT_URLCONF = 'notfellchen.urls' ROOT_URLCONF = 'notfellchen.urls'
@@ -222,6 +266,7 @@ TEMPLATES = [
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
# Needed for allauth
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media', 'django.template.context_processors.media',

View File

@@ -18,12 +18,14 @@ from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] path('accounts/', include('allauth.urls')),
] + debug_toolbar_urls()
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
path("", include("fellchensammlung.urls")), path("", include("fellchensammlung.urls")),

View File

@@ -47,7 +47,7 @@
<div class="block"> <div class="block">
<a class="button is-warning" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a> <a class="button is-warning" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a>
<a class="button is-link" <a class="button is-link"
href="{% url 'django_registration_register' %}">{% translate "Registrieren" %}</a> href="{% url 'account_signup' %}">{% translate "Registrieren" %}</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}