feat: Add general field-based status and migrate data

This commit is contained in:
2025-08-31 00:28:44 +02:00
parent 1c7d943a21
commit 70f077e393
9 changed files with 236 additions and 12 deletions

View File

@@ -173,7 +173,6 @@ admin.site.register(Image)
admin.site.register(ModerationAction) admin.site.register(ModerationAction)
admin.site.register(Language) admin.site.register(Language)
admin.site.register(Announcement) admin.site.register(Announcement)
admin.site.register(AdoptionNoticeStatus)
admin.site.register(Subscriptions) admin.site.register(Subscriptions)
admin.site.register(Log) admin.site.register(Log)
admin.site.register(Timestamp) admin.site.register(Timestamp)

View File

@@ -5,7 +5,7 @@ from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeG
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.db import transaction from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location, AdoptionNoticeStatus from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
from rest_framework import status, serializers from rest_framework import status, serializers
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -374,7 +374,7 @@ class LocationApiView(APIView):
class AdoptionNoticeGeoJSONView(ListAPIView): class AdoptionNoticeGeoJSONView(ListAPIView):
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter( queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE) adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.choices)
serializer_class = AdoptionNoticeGeoJSONSerializer serializer_class = AdoptionNoticeGeoJSONSerializer
renderer_classes = [GeoJSONRenderer] renderer_classes = [GeoJSONRenderer]

View File

@@ -0,0 +1,87 @@
# Generated by Django 5.2.1 on 2025-08-30 21:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0059_rescueorganization_twenty_id_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='adoptionnotice',
options={'permissions': [('create_active_adoption_notice', 'Can create an active adoption notice')], 'verbose_name': 'Vermittlung', 'verbose_name_plural': 'Vermittlungen'},
),
migrations.AlterModelOptions(
name='adoptionnoticestatus',
options={'verbose_name': 'Vermittlungsstatus', 'verbose_name_plural': 'Vermittlungsstati'},
),
migrations.AlterModelOptions(
name='animal',
options={'verbose_name': 'Tier', 'verbose_name_plural': 'Tiere'},
),
migrations.AlterModelOptions(
name='announcement',
options={'verbose_name': 'Banner', 'verbose_name_plural': 'Banner'},
),
migrations.AlterModelOptions(
name='comment',
options={'verbose_name': 'Kommentar', 'verbose_name_plural': 'Kommentare'},
),
migrations.AlterModelOptions(
name='image',
options={'verbose_name': 'Bild', 'verbose_name_plural': 'Bilder'},
),
migrations.AlterModelOptions(
name='importantlocation',
options={'verbose_name': 'Wichtiger Standort', 'verbose_name_plural': 'Wichtige Standorte'},
),
migrations.AlterModelOptions(
name='location',
options={'verbose_name': 'Standort', 'verbose_name_plural': 'Standorte'},
),
migrations.AlterModelOptions(
name='moderationaction',
options={'verbose_name': 'Moderationsaktion', 'verbose_name_plural': 'Moderationsaktionen'},
),
migrations.AlterModelOptions(
name='notification',
options={'verbose_name': 'Benachrichtigung', 'verbose_name_plural': 'Benachrichtigungen'},
),
migrations.AlterModelOptions(
name='report',
options={'verbose_name': 'Meldung', 'verbose_name_plural': 'Meldungen'},
),
migrations.AlterModelOptions(
name='rescueorganization',
options={'ordering': ['name'], 'verbose_name': 'Tierschutzorganisation', 'verbose_name_plural': 'Tierschutzorganisationen'},
),
migrations.AlterModelOptions(
name='rule',
options={'verbose_name': 'Regel', 'verbose_name_plural': 'Regeln'},
),
migrations.AlterModelOptions(
name='searchsubscription',
options={'verbose_name': 'Abonnierte Suche', 'verbose_name_plural': 'Abonnierte Suchen'},
),
migrations.AlterModelOptions(
name='speciesspecificurl',
options={'verbose_name': 'Tierartspezifische URL', 'verbose_name_plural': 'Tierartspezifische URLs'},
),
migrations.AlterModelOptions(
name='subscriptions',
options={'verbose_name': 'Abonnement', 'verbose_name_plural': 'Abonnements'},
),
migrations.AlterModelOptions(
name='timestamp',
options={'verbose_name': 'Zeitstempel', 'verbose_name_plural': 'Zeitstempel'},
),
migrations.AddField(
model_name='adoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], default='disabled_other', max_length=64, verbose_name='Status'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.1 on 2025-08-30 21:51
import logging
from django.db import migrations
def map_status(adoption_notice_status):
minor = adoption_notice_status.minor_status
if minor == "searching":
return "active_searching"
if minor == "interested":
return "active_interested"
if minor == "waiting_for_review":
return "awaiting_action_waiting_for_review"
if minor == "needs_additional_info":
return "awaiting_action_needs_additional_info"
if minor == "successful_with_notfellchen":
return "closed_successful_with_notfellchen"
if minor == "successful_without_notfellchen":
return "closed_successful_without_notfellchen"
if minor == "animal_died":
return "closed_animal_died"
if minor == "closed_for_other_adoption_notice":
return "closed_for_other_adoption_notice"
if minor == "not_open_for_adoption_anymore":
return "closed_not_open_for_adoption_anymore"
if minor == "other":
return "closed_other"
if minor == "against_the_rules":
return "disabled_against_the_rules"
if minor == "unchecked":
return "disabled_unchecked"
if minor in ["missing_information", "technical_error"]:
return "disabled_other"
return None
def migrate_status(apps, schema_editor):
# We can't import the model directly as it may be a newer
# version than this migration expects. We use the historical version.
AdoptionNoticeStatus = apps.get_model("fellchensammlung", "AdoptionNoticeStatus")
AdoptionNotice = apps.get_model("fellchensammlung", "AdoptionNotice")
for ans in AdoptionNoticeStatus.objects.all():
adoption_notice = AdoptionNotice.objects.get(id=ans.adoption_notice.id)
new_status = map_status(ans)
logging.debug(f"{ans.minor_status} -> {new_status}")
adoption_notice.adoption_notice_status = map_status(ans)
adoption_notice.save()
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0060_alter_adoptionnotice_options_and_more'),
]
operations = [
migrations.RunPython(migrate_status),
]

View File

@@ -12,12 +12,13 @@ from django.db.models.signals import post_save
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from sphinx.ext.inheritance_diagram import module_sig_re
from .tools import misc, geo from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_url from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position from .tools.geo import LocationProxy, Position
from .tools.misc import age_as_hr_string, time_since_as_hr_string from .tools.misc import age_as_hr_string, time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices
from .tools.model_helpers import ndm as NotificationDisplayMapping from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -395,6 +396,8 @@ class AdoptionNotice(models.Model):
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe")) location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, ) location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, )
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator')) owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator'))
adoption_notice_status = models.TextField(max_length=64, verbose_name=_('Status'),
choices=AdoptionNoticeStatusChoices.all_choices())
@property @property
def animals(self): def animals(self):

View File

@@ -10,7 +10,7 @@ from django.template.loader import render_to_string
from django.core import mail from django.core import mail
from django.utils.html import strip_tags from django.utils.html import strip_tags
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log, \ from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, Log, \
Notification, NotificationTypeChoices Notification, NotificationTypeChoices
from fellchensammlung.tools.misc import is_404 from fellchensammlung.tools.misc import is_404

View File

@@ -1,4 +1,4 @@
from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatus from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices
def gather_metrics_data(): def gather_metrics_data():
@@ -9,14 +9,14 @@ def gather_metrics_data():
"""Adoption notices""" """Adoption notices"""
num_adoption_notices = AdoptionNotice.objects.count() num_adoption_notices = AdoptionNotice.objects.count()
adoption_notices_active = AdoptionNotice.objects.filter( adoption_notices_active = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE) adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix
num_adoption_notices_active = adoption_notices_active.count() num_adoption_notices_active = adoption_notices_active.count()
num_adoption_notices_closed = AdoptionNotice.objects.filter( num_adoption_notices_closed = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count() adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix
num_adoption_notices_disabled = AdoptionNotice.objects.filter( num_adoption_notices_disabled = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.DISABLED).count() adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix
num_adoption_notices_awaiting_action = AdoptionNotice.objects.filter( num_adoption_notices_awaiting_action = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count() adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix
adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count() adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count()

View File

@@ -55,3 +55,74 @@ ndm = {NotificationTypeChoices.NEW_USER: NotificationDisplayMapping(
web_partial='fellchensammlung/partials/notifications/body-an-for-search.html' web_partial='fellchensammlung/partials/notifications/body-an-for-search.html'
) )
} }
class DescriptiveTextChoices(models.TextChoices):
class Descriptions:
pass
@classmethod
def get_description(cls, value):
return cls.Descriptions.__getattribute__(value, "")
class AdoptionNoticeStatusChoices:
class Active(DescriptiveTextChoices):
SEARCHING = "active_searching", _("Searching")
INTERESTED = "active_interested", _("Interested")
class Descriptions:
SEARCHING = ""
INTERESTED = _("Jemand hat bereits Interesse an den Tieren.")
class AwaitingAction(DescriptiveTextChoices):
WAITING_FOR_REVIEW = "awaiting_action_waiting_for_review", _("Waiting for review")
NEEDS_ADDITIONAL_INFO = "awaiting_action_needs_additional_info", _("Needs additional info")
class Descriptions:
WAITING_FOR_REVIEW = _("Deaktiviert bis Moderator*innen die Vermittlung prüfen können.")
NEEDS_ADDITIONAL_INFO = _("Deaktiviert bis Informationen nachgetragen werden.")
class Closed(DescriptiveTextChoices):
SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)")
SUCCESSFUL_WITHOUT_NOTFELLCHEN = "closed_successful_without_notfellchen", _("Successful (without Notfellchen)")
ANIMAL_DIED = "closed_animal_died", _("Animal died")
FOR_OTHER_ADOPTION_NOTICE = "closed_for_other_adoption_notice", _("Closed for other adoption notice")
NOT_OPEN_ANYMORE = "closed_not_open_for_adoption_anymore", _("Not open for adoption anymore")
OTHER = "closed_other", _("Other (closed)")
class Descriptions:
SUCCESSFUL_WITH_NOTFELLCHEN = _("Vermittlung erfolgreich abgeschlossen.")
SUCCESSFUL_WITHOUT_NOTFELLCHEN = _("Vermittlung erfolgreich abgeschlossen.")
ANIMAL_DIED = _("Die zu vermittelnden Tiere sind über die Regenbrücke gegangen.")
FOR_OTHER_ADOPTION_NOTICE = _("Vermittlung wurde zugunsten einer anderen geschlossen.")
NOT_OPEN_ANYMORE = _("Tier(e) stehen nicht mehr zur Vermittlung bereit.")
OTHER = _("Vermittlung geschlossen.")
class Disabled(DescriptiveTextChoices):
AGAINST_RULES = "disabled_against_the_rules", _("Against the rules")
UNCHECKED = "disabled_unchecked", _("Unchecked")
OTHER = "disabled_other", _("Other (disabled)")
class Descriptions:
AGAINST_RULES = _("Vermittlung deaktiviert da sie gegen die Regeln verstößt.")
UNCHECKED = _("Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde.")
OTHER = _("Vermittlung deaktiviert.")
@classmethod
def all_choices(cls):
"""Return all subgroup choices as a single list for use in models."""
return (
cls.Active.choices
+ cls.AwaitingAction.choices
+ cls.Closed.choices
+ cls.Disabled.choices
)
@classmethod
def get_description(cls, value):
"""Get description regardless of which subgroup the value belongs to."""
for subgroup in (cls.Active, cls.AwaitingAction, cls.Closed, cls.Disabled):
if value in subgroup.values:
return subgroup.get_description(value)
return ""

View File

@@ -21,7 +21,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, Notification, RescueOrganization, \ User, Location, Subscriptions, Notification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \ Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
@@ -36,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.model_helpers import AdoptionNoticeStatusChoices
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
@@ -59,7 +60,7 @@ def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=Trust
def index(request): def index(request):
"""View function for home page of site.""" """View function for home page of site."""
latest_adoption_list = AdoptionNotice.objects.filter( latest_adoption_list = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).order_by("-created_at") adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.choices).order_by("-created_at")
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active] active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
language_code = translation.get_language() language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code) lang = Language.objects.get(languagecode=language_code)