Compare commits
No commits in common. "fd3478600f0243ba50417e6e19da14eba17e5c44" and "e38234b7366a5f4afaa19de5a6d28c0fae1102db" have entirely different histories.
fd3478600f
...
e38234b736
@ -60,12 +60,3 @@ Now we can simply use the InfluxDB as data source in Grafana and configure until
|
|||||||
beautiful plots!
|
beautiful plots!
|
||||||
|
|
||||||
.. image:: monitoring_grafana.png
|
.. image:: monitoring_grafana.png
|
||||||
|
|
||||||
Healthchecks
|
|
||||||
------------
|
|
||||||
|
|
||||||
You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
|
|
||||||
Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
|
|
||||||
.. code::
|
|
||||||
[monitoring]
|
|
||||||
healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
|
|
||||||
|
@ -12,7 +12,10 @@ Notfellchen Plattform Dokumentation
|
|||||||
API/index.rst
|
API/index.rst
|
||||||
|
|
||||||
.. image:: rtfm.png
|
.. image:: rtfm.png
|
||||||
:name: Ratte lesend
|
:name: RTFM by Elektroll
|
||||||
:alt: Zeichnung einer lesenden Ratte
|
:scale: 50 %
|
||||||
|
:alt: Soviet style image of workers holding a sign with a gear and a screwdriver. Below is says "Read the manual"
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
|
||||||
|
Read the manual, Image by `Mike Powell (CC-BY) <https://elektroll.art/>`_.
|
||||||
|
BIN
docs/rtfm.png
BIN
docs/rtfm.png
Binary file not shown.
Before Width: | Height: | Size: 485 KiB After Width: | Height: | Size: 815 KiB |
@ -2,7 +2,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log
|
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice
|
||||||
|
|
||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||||
@ -62,4 +62,3 @@ admin.site.register(Text)
|
|||||||
admin.site.register(Announcement)
|
admin.site.register(Announcement)
|
||||||
admin.site.register(AdoptionNoticeStatus)
|
admin.site.register(AdoptionNoticeStatus)
|
||||||
admin.site.register(Subscriptions)
|
admin.site.register(Subscriptions)
|
||||||
admin.site.register(Log)
|
|
||||||
|
@ -6,7 +6,7 @@ from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportC
|
|||||||
Comment
|
Comment
|
||||||
from django_registration.forms import RegistrationForm
|
from django_registration.forms import RegistrationForm
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
|
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from notfellchen.settings import MEDIA_URL
|
from notfellchen.settings import MEDIA_URL
|
||||||
|
|
||||||
@ -142,8 +142,8 @@ class CommentForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_action = "comment"
|
||||||
self.helper.form_class = 'form-comments'
|
self.helper.form_class = 'form-comments'
|
||||||
self.helper.add_input(Hidden('action', 'comment'))
|
|
||||||
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-10-10 21:00
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0008_alter_adoptionnoticestatus_minor_status_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Log',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('action', models.CharField(max_length=255, verbose_name='Aktion')),
|
|
||||||
('text', models.CharField(max_length=1000, verbose_name='Log text')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -72,7 +72,7 @@ class User(AbstractUser):
|
|||||||
return self.get_absolute_url()
|
return self.get_absolute_url()
|
||||||
|
|
||||||
def get_num_unread_notifications(self):
|
def get_num_unread_notifications(self):
|
||||||
return BaseNotification.objects.filter(user=self, read=False).count()
|
return BaseNotification.objects.filter(user=self,read=False).count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self):
|
def owner(self):
|
||||||
@ -141,7 +141,6 @@ class Location(models.Model):
|
|||||||
instance.location = location
|
instance.location = location
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganization(models.Model):
|
class RescueOrganization(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
@ -162,10 +161,7 @@ class RescueOrganization(models.Model):
|
|||||||
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||||
allows_using_materials = models.CharField(max_length=200,
|
allows_using_materials = models.CharField(max_length=200,default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED], choices=ALLOW_USE_MATERIALS_CHOICE, verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
||||||
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
|
|
||||||
choices=ALLOW_USE_MATERIALS_CHOICE,
|
|
||||||
verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
|
||||||
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
||||||
@ -544,6 +540,7 @@ class Text(models.Model):
|
|||||||
return expandable_dict
|
return expandable_dict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Announcement(Text):
|
class Announcement(Text):
|
||||||
"""
|
"""
|
||||||
Class to store announcements that should be displayed for all users
|
Class to store announcements that should be displayed for all users
|
||||||
@ -642,17 +639,4 @@ class Subscriptions(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.owner} - {self.adoption_notice}"
|
return f"{self.owner} - { self.adoption_notice }"
|
||||||
|
|
||||||
|
|
||||||
class Log(models.Model):
|
|
||||||
"""
|
|
||||||
Basic class that allows logging random entries for later inspection
|
|
||||||
"""
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"))
|
|
||||||
action = models.CharField(max_length=255, verbose_name=_("Aktion"))
|
|
||||||
text = models.CharField(max_length=1000, verbose_name=_("Log text"))
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"[{self.action}] - {self.user} - {self.created_at.strftime('%H:%M:%S %d-%m-%Y ')}"
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from notfellchen.celery import app as celery_app
|
from notfellchen.celery import app as celery_app
|
||||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices
|
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices
|
||||||
from .tools.misc import healthcheck_ok
|
|
||||||
from .models import Location, AdoptionNotice
|
from .models import Location, AdoptionNotice
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +17,3 @@ def task_deactivate_unchecked():
|
|||||||
def add_adoption_notice_location(pk):
|
def add_adoption_notice_location(pk):
|
||||||
instance = AdoptionNotice.objects.get(pk=pk)
|
instance = AdoptionNotice.objects.get(pk=pk)
|
||||||
Location.add_location_to_object(instance)
|
Location.add_location_to_object(instance)
|
||||||
|
|
||||||
@celery_app.task(name="tools.healthcheck")
|
|
||||||
def task_healthcheck():
|
|
||||||
healthcheck_ok()
|
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import datetime as datetime
|
import datetime as datetime
|
||||||
import logging
|
|
||||||
|
|
||||||
from notfellchen import settings
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def pluralize(number, letter="e"):
|
def pluralize(number, letter="e"):
|
||||||
@ -15,11 +11,11 @@ def pluralize(number, letter="e"):
|
|||||||
|
|
||||||
def age_as_hr_string(age: datetime.timedelta) -> str:
|
def age_as_hr_string(age: datetime.timedelta) -> str:
|
||||||
days = age.days
|
days = age.days
|
||||||
weeks = age.days / 7
|
weeks = age.days/7
|
||||||
months = age.days / 30
|
months = age.days/30
|
||||||
years = age.days / 365
|
years = age.days/365
|
||||||
if years >= 1:
|
if years >= 1:
|
||||||
months = months - 12 * years
|
months = months - 12*years
|
||||||
return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}'
|
return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}'
|
||||||
elif months >= 3:
|
elif months >= 3:
|
||||||
return f'{months:.0f} Monat{pluralize(months)}'
|
return f'{months:.0f} Monat{pluralize(months)}'
|
||||||
@ -27,10 +23,3 @@ def age_as_hr_string(age: datetime.timedelta) -> str:
|
|||||||
return f'{weeks:.0f} Woche{pluralize(weeks, "n")}'
|
return f'{weeks:.0f} Woche{pluralize(weeks, "n")}'
|
||||||
else:
|
else:
|
||||||
return f'{days:.0f} Tag{pluralize(days)}'
|
return f'{days:.0f} Tag{pluralize(days)}'
|
||||||
|
|
||||||
|
|
||||||
def healthcheck_ok():
|
|
||||||
try:
|
|
||||||
requests.get(settings.HEALTHCHECK_URL, timeout=10)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logging.error("Ping to healthcheck-server failed: %s" % e)
|
|
||||||
|
@ -14,7 +14,7 @@ from notfellchen import settings
|
|||||||
from fellchensammlung import logger
|
from fellchensammlung import logger
|
||||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
||||||
Species, Log
|
Species
|
||||||
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
||||||
CommentForm, ReportCommentForm, AnimalForm, \
|
CommentForm, ReportCommentForm, AnimalForm, \
|
||||||
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
|
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
|
||||||
@ -93,10 +93,6 @@ def adoption_notice_detail(request, adoption_notice_id):
|
|||||||
comment_instance.user = request.user
|
comment_instance.user = request.user
|
||||||
comment_instance.save()
|
comment_instance.save()
|
||||||
|
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="comment",
|
|
||||||
text=f"{request.user} hat Kommentar {comment_instance.pk} zur Vermittlung {adoption_notice_id} hinzugefügt")
|
|
||||||
|
|
||||||
# Auto-subscribe user to adoption notice
|
# Auto-subscribe user to adoption notice
|
||||||
subscription, created = Subscriptions.objects.get_or_create(adoption_notice=adoption_notice,
|
subscription, created = Subscriptions.objects.get_or_create(adoption_notice=adoption_notice,
|
||||||
owner=request.user)
|
owner=request.user)
|
||||||
@ -144,9 +140,6 @@ def adoption_notice_edit(request, adoption_notice_id):
|
|||||||
location = Location.get_location_from_string(adoption_notice_instance.location_string)
|
location = Location.get_location_from_string(adoption_notice_instance.location_string)
|
||||||
adoption_notice_instance.location = location
|
adoption_notice_instance.location = location
|
||||||
adoption_notice_instance.save()
|
adoption_notice_instance.save()
|
||||||
|
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="adoption_notice_edit", text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
|
|
||||||
return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], ))
|
return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], ))
|
||||||
else:
|
else:
|
||||||
form = AdoptionNoticeForm(instance=adoption_notice)
|
form = AdoptionNoticeForm(instance=adoption_notice)
|
||||||
@ -223,10 +216,6 @@ def add_adoption_notice(request):
|
|||||||
Animal.objects.create(owner=request.user,
|
Animal.objects.create(owner=request.user,
|
||||||
name=f"{species} {i + 1}", adoption_notice=instance, species=species, sex=sex,
|
name=f"{species} {i + 1}", adoption_notice=instance, species=species, sex=sex,
|
||||||
date_of_birth=date_of_birth)
|
date_of_birth=date_of_birth)
|
||||||
|
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="add_adoption_notice",
|
|
||||||
text=f"{request.user} hat Vermittlung {instance.pk} hinzugefügt")
|
|
||||||
return redirect(reverse("adoption-notice-detail", args=[instance.pk]))
|
return redirect(reverse("adoption-notice-detail", args=[instance.pk]))
|
||||||
else:
|
else:
|
||||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
|
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
|
||||||
@ -271,11 +260,6 @@ def add_photo_to_animal(request, animal_id):
|
|||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
animal.photos.add(instance)
|
animal.photos.add(instance)
|
||||||
|
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
|
||||||
text=f"{request.user} hat Foto {instance.pk} zum Tier {animal.pk} hinzugefügt")
|
|
||||||
|
|
||||||
if "save-and-add-another" in request.POST:
|
if "save-and-add-another" in request.POST:
|
||||||
form = ImageForm(in_flow=True)
|
form = ImageForm(in_flow=True)
|
||||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||||
@ -299,9 +283,6 @@ def add_photo_to_adoption_notice(request, adoption_notice_id):
|
|||||||
instance.owner = request.user
|
instance.owner = request.user
|
||||||
instance.save()
|
instance.save()
|
||||||
adoption_notice.photos.add(instance)
|
adoption_notice.photos.add(instance)
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
|
||||||
text=f"{request.user} hat Foto {instance.pk} zur Vermittlung {adoption_notice.pk} hinzugefügt")
|
|
||||||
if "save-and-add-another" in request.POST:
|
if "save-and-add-another" in request.POST:
|
||||||
form = ImageForm(in_flow=True)
|
form = ImageForm(in_flow=True)
|
||||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||||
@ -328,10 +309,6 @@ def animal_edit(request, animal_id):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
animal = form.save()
|
animal = form.save()
|
||||||
|
|
||||||
"""Log"""
|
|
||||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
|
||||||
text=f"{request.user} hat Tier {animal.pk} zum Tier geändert")
|
|
||||||
return redirect(reverse("animal-detail", args=[animal.pk], ))
|
return redirect(reverse("animal-detail", args=[animal.pk], ))
|
||||||
else:
|
else:
|
||||||
form = AnimalForm(instance=animal)
|
form = AnimalForm(instance=animal)
|
||||||
@ -452,7 +429,6 @@ def modqueue(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def updatequeue(request):
|
def updatequeue(request):
|
||||||
#TODO: Make sure update can only be done for instances with permission
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
print(request.POST.get("adoption_notice_id"))
|
print(request.POST.get("adoption_notice_id"))
|
||||||
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
# <your_project>/celery.py
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from notfellchen import settings
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'notfellchen.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'notfellchen.settings')
|
||||||
|
|
||||||
@ -18,12 +19,7 @@ app.conf.beat_schedule = {
|
|||||||
},
|
},
|
||||||
'daily-deactivation': {
|
'daily-deactivation': {
|
||||||
'task': 'admin.deactivate_unchecked',
|
'task': 'admin.deactivate_unchecked',
|
||||||
'schedule': crontab(hour=1),
|
'schedule': 30,
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
|
||||||
# If a healthcheck is configured, this will send an hourly ping to the healthchecks server
|
|
||||||
app.conf.beat_schedule['hourly-healthcheck'] = {'task': 'tools.healthcheck',
|
|
||||||
'schedule': crontab(minute=32),
|
|
||||||
}
|
|
||||||
|
@ -84,8 +84,6 @@ LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
|
|||||||
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")
|
||||||
|
|
||||||
""" MONITORING """
|
|
||||||
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
|
||||||
|
|
||||||
""" GEOCODING """
|
""" GEOCODING """
|
||||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
|
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
|
||||||
|
Loading…
Reference in New Issue
Block a user