Compare commits

25 Commits

Author SHA1 Message Date
e64cc4bd5f Merge branch 'develop'
# Conflicts:
#	src/locale/en/LC_MESSAGES/django.po
2025-11-09 17:55:11 +01:00
a498971d66 feat: create directory in one go, don't use cache dir and use more standard entrypoint.sh 2025-11-09 17:36:48 +01:00
527ab07b6f trans: fis various 2025-11-08 00:33:35 +01:00
e2e236d650 feat: add button to download all logs 2025-11-08 00:00:54 +01:00
c4100a9ade trans: fix various 2025-11-07 18:17:53 +01:00
9511b46af0 feat: deactivate cache in dev config 2025-11-07 18:13:30 +01:00
5b906a7708 feat: add clean method 2025-11-07 18:12:53 +01:00
d68e836b57 trans: add various english translations 2025-11-07 18:12:37 +01:00
fe77f1da8d trans: add many translations 2025-11-07 17:08:13 +01:00
78b71690c0 feat: Allow toggling ongoing communication status 2025-11-07 14:02:22 +01:00
3b9ee95abc feat: Add log export functionality 2025-11-07 14:01:51 +01:00
b4e50364de fix: dependency 2025-11-03 22:22:22 +01:00
b014b3b227 fix: quotes 2025-11-03 22:22:12 +01:00
99bfe460ee fix: remove debug statement 2025-11-03 18:24:45 +01:00
d4c7caa42d fix: allow empty internal IP 2025-11-03 18:24:20 +01:00
32c8fc88cf feat: add debug command that prints settings 2025-11-03 18:22:11 +01:00
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
18 changed files with 3302 additions and 863 deletions

View File

@@ -9,15 +9,14 @@ RUN apt install gettext -y
RUN apt install libpq-dev gcc -y RUN apt install libpq-dev gcc -y
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN mkdir /app/data RUN mkdir /app/data/static -p
RUN mkdir /app/data/static
RUN mkdir /app/data/media RUN mkdir /app/data/media
RUN pip install -e . # Without the -e the library static folder will not be copied by collectstatic! RUN pip install --no-cache-dir -e . # Without the -e the library static folder will not be copied by collectstatic!
RUN nf collectstatic --noinput RUN nf collectstatic --noinput
RUN nf compilemessages --ignore venv RUN nf compilemessages --ignore venv
COPY docker/notfellchen.bash /bin/notfellchen COPY docker/entrypoint.sh /bin/notfellchen
EXPOSE 7345 EXPOSE 7345
CMD ["notfellchen"] CMD ["notfellchen"]

View File

@@ -8,6 +8,8 @@ host=localhost
[django] [django]
secret=CHANGE-ME secret=CHANGE-ME
debug=True debug=True
internal_ips=["127.0.0.1"]
cache=False
[database] [database]
backend=sqlite3 backend=sqlite3

View File

@@ -39,7 +39,9 @@ dependencies = [
"drf-spectacular[sidecar]", "drf-spectacular[sidecar]",
"django-widget-tweaks", "django-widget-tweaks",
"django-super-deduper", "django-super-deduper",
"django-allauth[mfa]" "django-allauth[mfa]",
"django_debug_toolbar",
"django-admin-extra-buttons"
] ]
dynamic = ["version", "readme"] dynamic = ["version", "readme"]

View File

@@ -7,6 +7,8 @@ from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from admin_extra_buttons.api import ExtraButtonsMixin, button, link
from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation, SocialMediaPost SpeciesSpecificURL, ImportantLocation, SocialMediaPost
@@ -17,6 +19,21 @@ from django.utils.translation import gettext_lazy as _
from .tools.model_helpers import AdoptionNoticeStatusChoices from .tools.model_helpers import AdoptionNoticeStatusChoices
def export_to_csv_generic(model, queryset):
meta = model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response
@admin.register(AdoptionNotice) @admin.register(AdoptionNotice)
class AdoptionNoticeAdmin(admin.ModelAdmin): class AdoptionNoticeAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "description__icontains") search_fields = ("name__icontains", "description__icontains")
@@ -49,17 +66,7 @@ class UserAdmin(admin.ModelAdmin):
return format_html('<a href="{}">{} Adoption Notices</a>', url, count) return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
def export_as_csv(self, request, queryset): def export_as_csv(self, request, queryset):
meta = self.model._meta response = export_to_csv_generic(self.model, queryset)
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response return response
export_as_csv.short_description = _("Ausgewählte User exportieren") export_as_csv.short_description = _("Ausgewählte User exportieren")
@@ -164,6 +171,28 @@ class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",) list_filter = ("platform",)
@admin.register(Log)
class LogAdmin(ExtraButtonsMixin, admin.ModelAdmin):
ordering = ["-created_at"]
list_filter = ("action",)
list_display = ("action", "user", "created_at")
actions = ("export_as_csv",)
@admin.action(description=_("Ausgewählte Logs exportieren"))
def export_as_csv(self, request, queryset):
response = export_to_csv_generic(Log, queryset)
return response
@button()
def export_all_as_csv(self, request):
actual_queryset = Log.objects.all()
response = export_to_csv_generic(Log, actual_queryset)
return response
@link(href="https://www.google.com/", visible=lambda btn: True)
def invisible(self, button):
button.visible = False
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 +201,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()
@@ -138,8 +141,12 @@ class ModerationActionForm(forms.ModelForm):
class AddedRegistrationForm(forms.Form): 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=_( 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.")) "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): def signup(self, request, user):
pass pass

View File

@@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from notfellchen import settings
class Command(BaseCommand):
help = 'Print the current settings'
def handle(self, *args, **options):
for key in settings.__dir__():
if key.startswith("_") or key == "SECRET_KEY":
continue
print(f"{key} = {getattr(settings, key)}")

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"]
@@ -425,7 +421,7 @@ class AdoptionNotice(models.Model):
def num_per_sex(self): def num_per_sex(self):
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 +511,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

@@ -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

@@ -71,7 +71,22 @@
value="{{ rescue_org.pk }}"> value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked"> <input type="hidden" name="action" value="checked">
<button class="" type="submit">{% translate "Organisation geprüft" %}</button> <button class="" type="submit">{% translate "Organisation geprüft" %}</button>
</form>
</div>
<div class="card-footer-item is-warning">
<form method="post">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="toggle_active_communication">
<button class="" type="submit">
{% if rescue_org.ongoing_communication %}
{% translate "Aktive Kommunikation beendet" %}
{% else %}
{% translate "Aktive Kommunikation" %}
{% endif %}
</button>
</form> </form>
</div> </div>
<div class="card-footer-item is-danger"> <div class="card-footer-item is-danger">

View File

@@ -1,9 +1,10 @@
{% 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 %}
{% if num_per_sex.F > 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.F }} </span> <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' %}">
@@ -11,9 +12,9 @@
</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"
@@ -22,18 +23,18 @@
</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' %}"
@@ -41,4 +42,5 @@
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% endwith %}
</div> </div>

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,23 @@ 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 clear(self):
self.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

@@ -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)
@@ -846,8 +866,10 @@ 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 == "toggle_active_communication":
rescue_org.ongoing_communication = not rescue_org.ongoing_communication
rescue_org.save()
elif action == "set_species_url": elif action == "set_species_url":
species_url_form = SpeciesURLForm(request.POST) species_url_form = SpeciesURLForm(request.POST)
@@ -858,6 +880,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,
@@ -906,6 +929,10 @@ def exclude_from_regular_check(request, rescue_organization_id, source="organiza
to_be_excluded = 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 = to_be_excluded rescue_org.exclude_from_check = to_be_excluded
rescue_org.save() rescue_org.save()
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)) return redirect(reverse(source))
else: else:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,20 @@ 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
internal_ip_raw_config_value = config.get("django", "internal_ips", fallback=None)
if internal_ip_raw_config_value:
INTERNAL_IPS = json.loads(internal_ip_raw_config_value)
# Cache
if config.getboolean('django', 'cache', fallback=False):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "uniques-snowflake",
}
}
""" 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")
@@ -141,7 +155,7 @@ AUTHENTICATION_BACKENDS = [
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_SIGNUP_FIELDS = ['username*', "email*", "password1*", "password2*"] ACCOUNT_SIGNUP_FIELDS = ['username*', "email*", "password1*"]
ACCOUNT_SIGNUP_FORM_CLASS = 'fellchensammlung.forms.AddedRegistrationForm' ACCOUNT_SIGNUP_FORM_CLASS = 'fellchensammlung.forms.AddedRegistrationForm'
@@ -229,16 +243,21 @@ INSTALLED_APPS = [
'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",
'admin_extra_buttons',
] ]
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',
# Needs to be after SessionMiddleware and before CommonMiddleware # Needs to be after SessionMiddleware and before CommonMiddleware
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
"django.middleware.cache.UpdateCacheMiddleware",
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
"django.middleware.cache.FetchFromCacheMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',

View File

@@ -18,13 +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')), path('accounts/', include('allauth.urls')),
] ] + debug_toolbar_urls()
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
path("", include("fellchensammlung.urls")), path("", include("fellchensammlung.urls")),