From c4da3318c23cd2a8c9525628844881e8ffecd682 Mon Sep 17 00:00:00 2001 From: moanos Date: Sun, 16 Nov 2025 18:49:01 +0100 Subject: [PATCH] feat: Add history tracking --- pyproject.toml | 3 +- src/fellchensammlung/admin.py | 30 +- ...doptionnotice_historicalanimal_and_more.py | 367 ++++++++++++++++++ src/fellchensammlung/models.py | 15 + src/notfellchen/settings.py | 2 + 5 files changed, 402 insertions(+), 15 deletions(-) create mode 100644 src/fellchensammlung/migrations/0071_historicaladoptionnotice_historicalanimal_and_more.py diff --git a/pyproject.toml b/pyproject.toml index 98513a4..c4411b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,8 @@ dependencies = [ "django-super-deduper", "django-allauth[mfa]", "django_debug_toolbar", - "django-admin-extra-buttons" + "django-admin-extra-buttons", + "django-simple-history" ] dynamic = ["version", "readme"] diff --git a/src/fellchensammlung/admin.py b/src/fellchensammlung/admin.py index 933f7e0..baaa827 100644 --- a/src/fellchensammlung/admin.py +++ b/src/fellchensammlung/admin.py @@ -8,6 +8,7 @@ from django.urls import reverse from django.utils.http import urlencode from admin_extra_buttons.api import ExtraButtonsMixin, button, link +from simple_history.admin import SimpleHistoryAdmin from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ SpeciesSpecificURL, ImportantLocation, SocialMediaPost @@ -50,7 +51,7 @@ class AdoptionNoticeAdmin(admin.ModelAdmin): # Re-register UserAdmin @admin.register(User) -class UserAdmin(admin.ModelAdmin): +class UserAdmin(SimpleHistoryAdmin): search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains") list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices") list_filter = ("is_active", "trust_level",) @@ -78,7 +79,7 @@ def _reported_content_link(obj): @admin.register(ReportComment) -class ReportCommentAdmin(admin.ModelAdmin): +class ReportCommentAdmin(SimpleHistoryAdmin): list_display = ["user_comment", "reported_content_link"] date_hierarchy = "created_at" @@ -89,7 +90,7 @@ class ReportCommentAdmin(admin.ModelAdmin): @admin.register(ReportAdoptionNotice) -class ReportAdoptionNoticeAdmin(admin.ModelAdmin): +class ReportAdoptionNoticeAdmin(SimpleHistoryAdmin): list_display = ["user_comment", "reported_content_link"] date_hierarchy = "created_at" @@ -104,7 +105,7 @@ class SpeciesSpecificURLInline(admin.StackedInline): @admin.register(RescueOrganization) -class RescueOrganizationAdmin(admin.ModelAdmin): +class RescueOrganizationAdmin(SimpleHistoryAdmin): search_fields = ("name", "description", "internal_comment", "location_string", "location__city") list_display = ("name", "trusted", "allows_using_materials", "website") list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter)) @@ -115,12 +116,12 @@ class RescueOrganizationAdmin(admin.ModelAdmin): @admin.register(Text) -class TextAdmin(admin.ModelAdmin): +class TextAdmin(SimpleHistoryAdmin): search_fields = ("title__icontains", "text_code__icontains",) @admin.register(Comment) -class CommentAdmin(admin.ModelAdmin): +class CommentAdmin(SimpleHistoryAdmin): list_filter = ("user",) @@ -130,7 +131,7 @@ class BaseNotificationAdmin(admin.ModelAdmin): @admin.register(SearchSubscription) -class SearchSubscriptionAdmin(admin.ModelAdmin): +class SearchSubscriptionAdmin(SimpleHistoryAdmin): list_filter = ("owner",) @@ -158,7 +159,7 @@ class IsImportantListFilter(admin.SimpleListFilter): @admin.register(Location) -class LocationAdmin(admin.ModelAdmin): +class LocationAdmin(SimpleHistoryAdmin): search_fields = ("name__icontains", "city__icontains") list_filter = [IsImportantListFilter] inlines = [ @@ -167,7 +168,7 @@ class LocationAdmin(admin.ModelAdmin): @admin.register(SocialMediaPost) -class SocialMediaPostAdmin(admin.ModelAdmin): +class SocialMediaPostAdmin(SimpleHistoryAdmin): list_filter = ("platform",) @@ -193,12 +194,13 @@ class LogAdmin(ExtraButtonsMixin, admin.ModelAdmin): def invisible(self, button): button.visible = False -admin.site.register(Animal) + +admin.site.register(Animal, SimpleHistoryAdmin) admin.site.register(Species) -admin.site.register(Rule) +admin.site.register(Rule, SimpleHistoryAdmin) admin.site.register(Image) -admin.site.register(ModerationAction) +admin.site.register(ModerationAction, SimpleHistoryAdmin) admin.site.register(Language) -admin.site.register(Announcement) -admin.site.register(Subscriptions) +admin.site.register(Announcement, SimpleHistoryAdmin) +admin.site.register(Subscriptions, SimpleHistoryAdmin) admin.site.register(Timestamp) diff --git a/src/fellchensammlung/migrations/0071_historicaladoptionnotice_historicalanimal_and_more.py b/src/fellchensammlung/migrations/0071_historicaladoptionnotice_historicalanimal_and_more.py new file mode 100644 index 0000000..3f758b8 --- /dev/null +++ b/src/fellchensammlung/migrations/0071_historicaladoptionnotice_historicalanimal_and_more.py @@ -0,0 +1,367 @@ +# Generated by Django 5.2.8 on 2025-11-16 17:37 + +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import simple_history.models +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fellchensammlung', '0070_user_mod_notes_alter_user_reason_for_signup'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalAdoptionNotice', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('last_checked', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am')), + ('searching_since', models.DateField(verbose_name='Sucht nach einem Zuhause seit')), + ('name', models.CharField(max_length=200, verbose_name='Titel der Vermittlung')), + ('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')), + ('further_information', models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims)', null=True, verbose_name='Link zu mehr Informationen')), + ('group_only', models.BooleanField(default=False, verbose_name='Ausschließlich Gruppenadoption')), + ('location_string', models.CharField(max_length=200, verbose_name='Ortsangabe')), + ('adoption_notice_status', 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'), ('awaiting_action_unchecked', 'Unchecked'), ('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_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status')), + ('adoption_process', models.TextField(blank=True, choices=[('contact_person_in_an', 'Kontaktiere die Person im Vermittlungstext')], max_length=64, null=True, verbose_name='Adoptionsprozess')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')), + ('organization', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization', verbose_name='Organisation')), + ('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ], + options={ + 'verbose_name': 'historical Vermittlung', + 'verbose_name_plural': 'historical Vermittlungen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalAnimal', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('date_of_birth', models.DateField(verbose_name='Geburtsdatum')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')), + ('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20, verbose_name='Geschlecht')), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('species', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.species', verbose_name='Tierart')), + ], + options={ + 'verbose_name': 'historical Tier', + 'verbose_name_plural': 'historical Tiere', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalAnnouncement', + fields=[ + ('text_ptr', models.ForeignKey(auto_created=True, blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, parent_link=True, related_name='+', to='fellchensammlung.text')), + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='Titel')), + ('content', models.TextField(verbose_name='Inhalt')), + ('text_code', models.CharField(blank=True, max_length=24, verbose_name='Text code')), + ('logged_in_only', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('publish_start_time', models.DateTimeField(verbose_name='Veröffentlichungszeitpunkt')), + ('publish_end_time', models.DateTimeField(verbose_name='Veröffentlichungsende')), + ('type', models.CharField(choices=[('important', 'important'), ('warning', 'warning'), ('info', 'info')], default='info', max_length=100)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')), + ], + options={ + 'verbose_name': 'historical Banner', + 'verbose_name_plural': 'historical Banner', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalComment', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('text', models.TextField(verbose_name='Inhalt')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('reply_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.comment', verbose_name='Antwort auf')), + ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')), + ], + options={ + 'verbose_name': 'historical Kommentar', + 'verbose_name_plural': 'historical Kommentare', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalModerationAction', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('action', models.CharField(choices=[('user_banned', 'User was banned'), ('content_deleted', 'Content was deleted'), ('comment', 'Comment was added'), ('other_action_taken', 'Other action was taken'), ('no_action_taken', 'No action was taken')], max_length=30)), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('public_comment', models.TextField(blank=True)), + ('private_comment', models.TextField(blank=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('report', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.report')), + ], + options={ + 'verbose_name': 'historical Moderationsaktion', + 'verbose_name_plural': 'historical Moderationsaktionen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalReport', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, help_text='ID dieses reports', verbose_name='ID')), + ('status', models.CharField(choices=[('action taken', 'Action was taken'), ('no action taken', 'No action was taken'), ('waiting', 'Waiting for moderator action')], max_length=30)), + ('user_comment', models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information')), + ('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')), + ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Meldung', + 'verbose_name_plural': 'historical Meldungen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalRescueOrganization', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('trusted', models.BooleanField(default=False, verbose_name='Vertrauenswürdig')), + ('allows_using_materials', models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten')), + ('location_string', models.CharField(blank=True, max_length=200, null=True, verbose_name='Ort der Organisation')), + ('instagram', models.URLField(blank=True, null=True, verbose_name='Instagram Profil')), + ('facebook', models.URLField(blank=True, null=True, verbose_name='Facebook Profil')), + ('fediverse_profile', models.URLField(blank=True, null=True, verbose_name='Fediverse Profil')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')), + ('phone_number', models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer')), + ('website', models.URLField(blank=True, null=True, verbose_name='Website')), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('last_checked', models.DateTimeField(blank=True, editable=False, verbose_name='Datum der letzten Prüfung')), + ('internal_comment', models.TextField(blank=True, null=True, verbose_name='Interner Kommentar')), + ('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')), + ('external_object_identifier', models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier')), + ('external_source_identifier', models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier')), + ('exclude_from_check', models.BooleanField(default=False, help_text='Organisation von der manuellen Überprüfung ausschließen, z.B. weil Tiere nicht online geführt werden', verbose_name='Von Prüfung ausschließen')), + ('regular_check_status', models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung')), + ('ongoing_communication', models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation')), + ('twenty_id', models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')), + ('parent_org', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization')), + ], + options={ + 'verbose_name': 'historical Tierschutzorganisation', + 'verbose_name_plural': 'historical Tierschutzorganisationen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalRule', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('rule_text', models.TextField(verbose_name='Regeltext')), + ('rule_identifier', models.CharField(help_text='Ein eindeutiger Identifikator der Regel. Ein Regelobjekt derselben Regel in einer anderen Sprache muss den gleichen Identifikator haben', max_length=24, verbose_name='Regel-ID')), + ('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')), + ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')), + ], + options={ + 'verbose_name': 'historical Regel', + 'verbose_name_plural': 'historical Regeln', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalSearchSubscription', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20, verbose_name='Geschlecht')), + ('max_distance', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True)), + ('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')), + ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')), + ('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Abonnierte Suche', + 'verbose_name_plural': 'historical Abonnierte Suchen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalSocialMediaPost', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')), + ('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')), + ('url', models.URLField(verbose_name='URL')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical social media post', + 'verbose_name_plural': 'historical social media posts', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalSubscriptions', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created_at', models.DateTimeField(blank=True, editable=False)), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, help_text='Vermittlung die abonniert wurde', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')), + ], + options={ + 'verbose_name': 'historical Abonnement', + 'verbose_name_plural': 'historical Abonnements', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalText', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='Titel')), + ('content', models.TextField(verbose_name='Inhalt')), + ('text_code', models.CharField(blank=True, max_length=24, verbose_name='Text code')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')), + ], + options={ + 'verbose_name': 'historical Text', + 'verbose_name_plural': 'historical Texte', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalUser', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(db_index=True, error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('trust_level', models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1)), + ('updated_at', models.DateTimeField(blank=True, editable=False)), + ('reason_for_signup', models.TextField(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.", verbose_name='Grund für die Registrierung')), + ('mod_notes', models.TextField(blank=True, null=True, verbose_name='Moderationsnotizen')), + ('email_notifications', models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('organization_affiliation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization', verbose_name='Organisation')), + ('preferred_language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Bevorzugte Sprache')), + ], + options={ + 'verbose_name': 'historical Nutzer*in', + 'verbose_name_plural': 'historical Nutzer*innen', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index 0df8e64..02a9f4b 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import Group from django.contrib.auth.models import AbstractUser from django.core.exceptions import ValidationError import base64 +from simple_history.models import HistoricalRecords from .tools import misc, geo from notfellchen.settings import MEDIA_URL, base_url @@ -187,6 +188,7 @@ class RescueOrganization(models.Model): specializations = models.ManyToManyField(Species, blank=True) twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True, help_text=_("ID der der Organisation in Twenty")) + history = HistoricalRecords() class Meta: unique_together = ('external_object_identifier', 'external_source_identifier',) @@ -250,6 +252,7 @@ class RescueOrganization(models.Model): def set_checked(self): self.last_checked = timezone.now() + self._change_reason = 'Organization checked' self.save() @property @@ -311,6 +314,7 @@ class User(AbstractUser): reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text) mod_notes = models.TextField(verbose_name=_("Moderationsnotizen"), null=True, blank=True) email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True) + history = HistoricalRecords() REQUIRED_FIELDS = ["reason_for_signup", "email"] class Meta: @@ -406,6 +410,7 @@ class AdoptionNotice(models.Model): adoption_process = models.TextField(null=True, blank=True, max_length=64, verbose_name=_('Adoptionsprozess'), choices=AdoptionProcess) + history = HistoricalRecords() @property def animals(self): @@ -641,6 +646,7 @@ class Animal(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE) updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) + history = HistoricalRecords() def __str__(self): return f"{self.name}" @@ -704,6 +710,7 @@ class SearchSubscription(models.Model): max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) + history = HistoricalRecords() def __str__(self): if self.location and self.max_distance: @@ -734,6 +741,7 @@ class Rule(models.Model): "Identifikator haben")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) + history = HistoricalRecords() def __str__(self): return self.title @@ -759,6 +767,7 @@ class Report(models.Model): user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) + history = HistoricalRecords() def __str__(self): return f"[{self.status}]: {self.user_comment:.20}" @@ -845,6 +854,7 @@ class ModerationAction(models.Model): # Only visible to moderator private_comment = models.TextField(blank=True) report = models.ForeignKey(Report, on_delete=models.CASCADE) + history = HistoricalRecords() def __str__(self): return f"[{self.action}]: {self.public_comment}" @@ -866,6 +876,7 @@ class Text(models.Model): content = models.TextField(verbose_name="Inhalt") language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT) text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True) + history = HistoricalRecords() class Meta: verbose_name = "Text" @@ -909,6 +920,7 @@ class Announcement(Text): INFO: "info", } type = models.CharField(choices=TYPES, max_length=100, default=INFO) + history = HistoricalRecords() @property def is_active(self): @@ -955,6 +967,7 @@ class Comment(models.Model): adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) text = models.TextField(verbose_name="Inhalt") reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE) + history = HistoricalRecords() def __str__(self): return f"{self.user} at {self.created_at.strftime('%H:%M %d.%m.%y')}: {self.text:.10}" @@ -1026,6 +1039,7 @@ class Subscriptions(models.Model): help_text=_("Vermittlung die abonniert wurde")) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + history = HistoricalRecords() def __str__(self): return f"{self.owner} - {self.adoption_notice}" @@ -1087,6 +1101,7 @@ class SocialMediaPost(models.Model): choices=PlatformChoices.choices) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) url = models.URLField(verbose_name=_("URL")) + history = HistoricalRecords() @staticmethod def get_an_to_post(): diff --git a/src/notfellchen/settings.py b/src/notfellchen/settings.py index 7d4586c..305316f 100644 --- a/src/notfellchen/settings.py +++ b/src/notfellchen/settings.py @@ -237,6 +237,7 @@ INSTALLED_APPS = [ 'widget_tweaks', "debug_toolbar", 'admin_extra_buttons', + 'simple_history', ] MIDDLEWARE = [ @@ -254,6 +255,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', # allauth middleware, needs to be after message middleware "allauth.account.middleware.AccountMiddleware", + 'simple_history.middleware.HistoryRequestMiddleware', ] ROOT_URLCONF = 'notfellchen.urls'