From 70f077e393b29206f5e1935fc677e479eeb4c745 Mon Sep 17 00:00:00 2001 From: moanos Date: Sun, 31 Aug 2025 00:28:44 +0200 Subject: [PATCH] feat: Add general field-based status and migrate data --- src/fellchensammlung/admin.py | 1 - src/fellchensammlung/api/views.py | 4 +- ...0_alter_adoptionnotice_options_and_more.py | 87 +++++++++++++++++++ ...061_datamigration_status_model_to_field.py | 63 ++++++++++++++ src/fellchensammlung/models.py | 5 +- src/fellchensammlung/tools/admin.py | 2 +- src/fellchensammlung/tools/metrics.py | 10 +-- src/fellchensammlung/tools/model_helpers.py | 71 +++++++++++++++ src/fellchensammlung/views.py | 5 +- 9 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 src/fellchensammlung/migrations/0060_alter_adoptionnotice_options_and_more.py create mode 100644 src/fellchensammlung/migrations/0061_datamigration_status_model_to_field.py diff --git a/src/fellchensammlung/admin.py b/src/fellchensammlung/admin.py index e91eb7e..37a901f 100644 --- a/src/fellchensammlung/admin.py +++ b/src/fellchensammlung/admin.py @@ -173,7 +173,6 @@ admin.site.register(Image) admin.site.register(ModerationAction) admin.site.register(Language) admin.site.register(Announcement) -admin.site.register(AdoptionNoticeStatus) admin.site.register(Subscriptions) admin.site.register(Log) admin.site.register(Timestamp) diff --git a/src/fellchensammlung/api/views.py b/src/fellchensammlung/api/views.py index d70db4d..b518f02 100644 --- a/src/fellchensammlung/api/views.py +++ b/src/fellchensammlung/api/views.py @@ -5,7 +5,7 @@ from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeG from rest_framework.views import APIView from rest_framework.response import Response 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 rest_framework import status, serializers from rest_framework.permissions import IsAuthenticated @@ -374,7 +374,7 @@ class LocationApiView(APIView): class AdoptionNoticeGeoJSONView(ListAPIView): 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 renderer_classes = [GeoJSONRenderer] diff --git a/src/fellchensammlung/migrations/0060_alter_adoptionnotice_options_and_more.py b/src/fellchensammlung/migrations/0060_alter_adoptionnotice_options_and_more.py new file mode 100644 index 0000000..73c4762 --- /dev/null +++ b/src/fellchensammlung/migrations/0060_alter_adoptionnotice_options_and_more.py @@ -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, + ), + ] diff --git a/src/fellchensammlung/migrations/0061_datamigration_status_model_to_field.py b/src/fellchensammlung/migrations/0061_datamigration_status_model_to_field.py new file mode 100644 index 0000000..64b27fb --- /dev/null +++ b/src/fellchensammlung/migrations/0061_datamigration_status_model_to_field.py @@ -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), + ] diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index a437b04..a113755 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -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 AbstractUser from django.core.exceptions import ValidationError +from sphinx.ext.inheritance_diagram import module_sig_re from .tools import misc, geo from notfellchen.settings import MEDIA_URL, base_url from .tools.geo import LocationProxy, Position 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 @@ -395,6 +396,8 @@ class AdoptionNotice(models.Model): location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe")) location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, ) 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 def animals(self): diff --git a/src/fellchensammlung/tools/admin.py b/src/fellchensammlung/tools/admin.py index 0c8073c..1753ba1 100644 --- a/src/fellchensammlung/tools/admin.py +++ b/src/fellchensammlung/tools/admin.py @@ -10,7 +10,7 @@ from django.template.loader import render_to_string from django.core import mail 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 from fellchensammlung.tools.misc import is_404 diff --git a/src/fellchensammlung/tools/metrics.py b/src/fellchensammlung/tools/metrics.py index 1175722..36a05d4 100644 --- a/src/fellchensammlung/tools/metrics.py +++ b/src/fellchensammlung/tools/metrics.py @@ -1,4 +1,4 @@ -from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatus +from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices def gather_metrics_data(): @@ -9,14 +9,14 @@ def gather_metrics_data(): """Adoption notices""" num_adoption_notices = AdoptionNotice.objects.count() 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_closed = AdoptionNotice.objects.filter( - adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count() + adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix 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( - adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count() + adoptionnoticestatus=AdoptionNoticeStatusChoices.all_choices()) # TODO fix adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count() diff --git a/src/fellchensammlung/tools/model_helpers.py b/src/fellchensammlung/tools/model_helpers.py index 4c12fe9..f8e8dec 100644 --- a/src/fellchensammlung/tools/model_helpers.py +++ b/src/fellchensammlung/tools/model_helpers.py @@ -55,3 +55,74 @@ ndm = {NotificationTypeChoices.NEW_USER: NotificationDisplayMapping( 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 "" diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index e4bf087..b231629 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -21,7 +21,7 @@ from notfellchen import settings from fellchensammlung import logger 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, \ ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost 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 rest_framework.authtoken.models import Token +from .tools.model_helpers import AdoptionNoticeStatusChoices 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): """View function for home page of site.""" 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] language_code = translation.get_language() lang = Language.objects.get(languagecode=language_code)