diff --git a/README.md b/README.md index 93b7e9d..42f21ab 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,4 @@ und für die öffentliche Rechercheplattform. - Python 3.14 (funktioniert wahrscheinlich auch mit anderen Versionen) - Django 6.0 - django-taggit +- django-taggit-helpers (uralt, funktioniert aber) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4c36c46 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# Installation zum Testen & Entwickeln + +Wir entwickeln auf Python 3.14. Python 3.12+ funktioniert, ältere haben zumindest Probleme mit Frescobaldi, was hiermit nichts zu tun hat… + +- `git clone ssh://git@git.zahlenlabyrinth.de:9922/Notenbund/liederquelle.git` +- `cd liederquelle` +- Python virtual environment (Venv) anlegen: `python3.14 -m venv .venv` +- Venv aktivieren: `. .venv/bin/activate` +- Abhängigkeiten installieren: `pip install -Ur requirements.txt` +- `cd liederquelle` +- `.env` anlegen: + + $ python manage.py shell + >> from django.core.management.utils import get_random_secret_key + >> with open('.env', 'w') as dotenv: + dotenv.write('SECRET_KEY=%s' % get_random_secret_key()) + >> ^D + +- Datenbank (SQLite) anlegen: `python3 manage.py makemigrations quellen; python3 manage.py migrate` +- Admin anlegen: `python3 manage.py createsuperuser` +- Server starten: `python3 manage.py runserver` +- Öffne [http://localhost:8080/admin] im Browser diff --git a/liederquelle/liederquelle/settings/base.py b/liederquelle/liederquelle/settings/base.py index 4988d83..25d29a8 100644 --- a/liederquelle/liederquelle/settings/base.py +++ b/liederquelle/liederquelle/settings/base.py @@ -5,6 +5,7 @@ see https://docs.djangoproject.com/en/6.0/ref/settings/ """ import os from pathlib import Path +from django.core.exceptions import ImproperlyConfigured # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -36,6 +37,7 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ 'quellen.apps.QuellenConfig', 'taggit', + 'taggit_helpers', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/liederquelle/manage.py b/liederquelle/manage.py index e192ea5..7f9038e 100755 --- a/liederquelle/manage.py +++ b/liederquelle/manage.py @@ -13,7 +13,7 @@ def main(): if not os.path.isfile(env): print("Configuration environment file (.env) not found!") sys.exit(1) - dotenv.load_dotenv(env) + dotenv.read_dotenv(env) os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'liederquelle.settings') try: diff --git a/liederquelle/quellen/admin.py b/liederquelle/quellen/admin.py index f5b58d5..1b2399d 100644 --- a/liederquelle/quellen/admin.py +++ b/liederquelle/quellen/admin.py @@ -1,31 +1,62 @@ from django.contrib import admin -from .models import Person, Identity, AuthorID, Contact, Medium +from .models import Person, Identity, AuthorID, AuthorURL, Contact, Medium from taggit_helpers.admin import TaggitListFilter, TaggitTabularInline - +from django.contrib.contenttypes.admin import GenericTabularInline, GenericStackedInline # TODO: Links zu Suchseiten mit Vorbelegungen -class IdentityTabularInline(admin.TabularInline): +class IdentityInline(admin.TabularInline): model = Identity - #ordering_field = ['slideshow', 'weight'] - ordering_field_hide_input = False - #fields = ['weight', 'display_title', 'target', 'image', 'overlay_image'] - # exclude = ['display_content', 'overlay_tilt', 'overlay_class', 'activated'] + ordering_field = ['alias',] + min_num = 1 + extra = 1 + show_change_link = True + #fields = ['author_id', 'alias', 'organization', 'tags'] + + +class AuthorIDInline(admin.TabularInline): + model = AuthorID + ordering_field = ['key',] + extra = 1 + + +class AuthorURLInline(admin.TabularInline): + model = AuthorURL + ordering_field = ['name',] + extra = 1 + + +class ContactInline(GenericStackedInline): + model = Contact + ordering_field = ['last_try',] + min_num = 1 + extra = 0 + show_change_link = True + + +class MediumInline(admin.TabularInline): + model = Medium + ordering_field = ['key',] + #min_num = 1 + #show_change_link = True @admin.register(Person) class PersonAdmin(admin.ModelAdmin): - list_display = ['name', 'full_name', 'birth_year'] + list_display = Person.list_display search_fields = ('name', 'full_name', 'name_native', 'birth_name') inlines = [ - IdentityTabularInline + IdentityInline, + AuthorIDInline, + AuthorURLInline, + ContactInline ] @admin.register(Identity) class IdentityAdmin(admin.ModelAdmin): - list_display = ['person', 'author_id', 'alias', 'organization'] + list_display = Identity.list_display search_fields = ('alias', 'organization') list_filter = ['person', 'organization', TaggitListFilter] exclude = ('tags',) @@ -36,19 +67,28 @@ class IdentityAdmin(admin.ModelAdmin): @admin.register(AuthorID) class AuthorIDAdmin(admin.ModelAdmin): - list_display = ['person','key','value'] + list_display = AuthorID.list_display search_fields = ('value',) +@admin.register(AuthorURL) +class AuthorURLAdmin(admin.ModelAdmin): + list_display = AuthorURL.list_display + search_fields = ('url',) + + @admin.register(Contact) class ContactAdmin(admin.ModelAdmin): date_hierarchy = 'last_try' - list_display = ['reference', 'name',] + list_display = Contact.list_display search_fields = ('name', 'address', 'remarks') #prepopulated_fields = {'user': ('',)} + inlines = [ + MediumInline, + ] @admin.register(Medium) class MediumAdmin(admin.ModelAdmin): - list_display = ['contact','key','value'] + list_display = Medium.list_display search_fields = ('value',) diff --git a/liederquelle/quellen/migrations/0001_initial.py b/liederquelle/quellen/migrations/0001_initial.py index ec9c46d..adffbc7 100644 --- a/liederquelle/quellen/migrations/0001_initial.py +++ b/liederquelle/quellen/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 6.0.1 on 2026-01-24 23:09 +# Generated by Django 6.0.1 on 2026-01-25 20:38 import django.db.models.deletion +import quellen.models import taggit.managers from django.conf import settings from django.db import migrations, models @@ -23,33 +24,50 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='Klarname, wie er beim Lied angezeigt werden soll. Normalerweise "Vorname Nachname".', max_length=127, verbose_name='Name')), ('full_name', models.CharField(blank=True, help_text='Name mit zusätzlichen Vornamen, Titeln usw.', max_length=255, verbose_name='Vollständiger Name')), + ('name_native', models.CharField(blank=True, help_text='Name in nichtlateinischer Schrift', max_length=255, verbose_name='Name in Originalschreibweise')), ('birth_name', models.CharField(blank=True, help_text='Name vor einer Namensänderung, z.B. durch Ehe.', max_length=127, verbose_name='Geburtsname')), ('details_secret', models.BooleanField(default=False, help_text='Beim Lied soll nur der Alias angezeigt werden, kein Klarname und keine Lebensdaten.', verbose_name='Details geheim')), ('birth_year', models.CharField(blank=True, help_text='Auch ungefähre Angaben sind erlaubt.', max_length=15, verbose_name='Geburtsjahr')), ('death_year', models.CharField(blank=True, help_text='Auch ungefähre Angaben sind erlaubt.', max_length=15, verbose_name='Todesjahr')), ('remarks', models.TextField(blank=True, verbose_name='Anmerkungen')), ], + options={ + 'verbose_name': 'Person', + 'verbose_name_plural': 'Personen', + 'ordering': ['name', 'birth_year'], + }, ), migrations.CreateModel( name='Contact', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveBigIntegerField(default=1)), + ('name', models.CharField(blank=True, help_text='Name der Kontaktperson, falls nicht mit der Person identisch', max_length=127, verbose_name='Kontaktperson')), ('address', models.TextField(blank=True, help_text='Postadresse', verbose_name='Adresse')), ('last_try', models.DateField(blank=True, null=True, verbose_name='letzter Kontaktversuch')), ('success', models.BooleanField(blank=True, default=None, help_text='War der Kontaktversuch erfolgreich?', null=True, verbose_name='Kontakt erfolgreich?')), ('remarks', models.TextField(blank=True, verbose_name='Anmerkungen')), - ('reference', models.ForeignKey(help_text='Person oder Verlag', on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('user', models.ForeignKey(blank=True, help_text='Kontakt durch Projektmitarbeiter*in', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('content_type', models.ForeignKey(default=quellen.models.Person, help_text='Person oder Verlag', on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('user', models.ForeignKey(blank=True, help_text='Kontakt durch bzw. persönlich bekannt mit Projektmitarbeiter*in', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), ], + options={ + 'verbose_name': 'Kontakt', + 'verbose_name_plural': 'Kontakte', + }, ), migrations.CreateModel( name='Medium', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(choices=[('Email', 'E-Mail'), ('Phone', 'Telefon'), ('Signal', 'Signal-ID')], default='Email', max_length=15, verbose_name='Art')), + ('key', models.CharField(choices=[('Email', 'E-Mail'), ('Phone', 'Telefon'), ('Web', 'Homepage'), ('Signal', 'Signal-ID'), ('Fedi', 'Fediverse-Handle'), ('Matrix', 'Matrix-ID'), ('XMPP', 'XMPP-ID')], default='Email', max_length=15, verbose_name='Art')), ('value', models.CharField(help_text='Adresse bzw. Handle', max_length=127, verbose_name='Wert')), ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quellen.contact')), ], + options={ + 'verbose_name': 'Kontaktmedium', + 'verbose_name_plural': 'Kontaktmedien', + 'ordering': ['contact', 'key'], + }, ), migrations.CreateModel( name='Identity', @@ -57,9 +75,29 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('author_id', models.CharField(help_text='Wie in der LilyPond-Datei und der `authors.yml` verwendet.', max_length=127, verbose_name='Autoren-ID')), ('alias', models.CharField(blank=True, help_text='Fahrtenname, Künstlername o.ä.; Spitznamen nur, wenn sie mehr sind als eine Vornamensvariante.', max_length=127, verbose_name='Alias')), - ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), + ('organization', models.CharField(blank=True, help_text='Bund (mit Stamm/Orden), Band o.ä. wie am Lied anzuzeigen – bitte zusätzlich taggen!', max_length=127, verbose_name='Organisation')), + ('tags', taggit.managers.TaggableManager(blank=True, help_text='Kommaseparierte Stichwörter zu dieser Identität, z.B. "Bund:DPB,Band:Schlagsaite"', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Stichwörter')), ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quellen.person')), ], + options={ + 'verbose_name': 'Identität', + 'verbose_name_plural': 'Identitäten', + 'ordering': ['author_id'], + }, + ), + migrations.CreateModel( + name='AuthorURL', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Homepage', help_text='z.B. Wikipedia-Eintrag', max_length=15, verbose_name='Art')), + ('url', models.URLField(help_text='Web-Adresse', verbose_name='URL')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quellen.person')), + ], + options={ + 'verbose_name': 'Autor-Website', + 'verbose_name_plural': 'Autor-Webseiten', + 'ordering': ['person', 'name'], + }, ), migrations.CreateModel( name='AuthorID', @@ -69,5 +107,14 @@ class Migration(migrations.Migration): ('value', models.CharField(help_text='ID in der Schreibweise der jeweiligen Datenbank', max_length=127, verbose_name='Wert')), ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quellen.person')), ], + options={ + 'verbose_name': 'Autor-ID', + 'verbose_name_plural': 'Autor-IDs', + 'ordering': ['person', 'key', 'value'], + }, + ), + migrations.AddIndex( + model_name='contact', + index=models.Index(fields=['content_type', 'object_id'], name='quellen_con_content_7ef419_idx'), ), ] diff --git a/liederquelle/quellen/models.py b/liederquelle/quellen/models.py index 79a56fd..0bc2250 100644 --- a/liederquelle/quellen/models.py +++ b/liederquelle/quellen/models.py @@ -1,6 +1,6 @@ from django.db import models from taggit.managers import TaggableManager -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User @@ -46,14 +46,14 @@ class Person(models.Model): blank=True, #help_text='', ) - # TODO: URLs wie Tags? + contacts = GenericRelation('Contact') class Meta: verbose_name = 'Person' verbose_name_plural = 'Personen' ordering = ['name','birth_year'] - list_display = ['name'] + list_display = ['name', 'full_name', 'birth_year'] def __str__(self): return self.name @@ -92,7 +92,7 @@ class Identity(models.Model): verbose_name_plural = 'Identitäten' ordering = ['author_id'] - list_display = ['author_id', 'alias'] + list_display = ['person', 'author_id', 'alias', 'organization'] def __str__(self): return "{} ({})".format(self.author_id, self.alias) @@ -103,7 +103,6 @@ class AuthorID(models.Model): blank=False, on_delete=models.CASCADE, ) - # TODO: Medien in eigene Tabelle ausgliedern class IDs(models.TextChoices): DISCOGS = "DISCOGS", 'Discogs' GND = "GND", 'DNB GND' @@ -134,12 +133,44 @@ class AuthorID(models.Model): list_display = ['person', 'key', 'value'] -class Contact(models.Model): - reference = models.ForeignKey(ContentType, - on_delete = models.CASCADE, - help_text = 'Person oder Verlag' - # TODO: Das stimmt so nicht, ID des Objekts fehlt +class AuthorURL(models.Model): + person = models.ForeignKey(Person, + blank=False, + on_delete=models.CASCADE, ) + name = models.CharField( + verbose_name='Art', + max_length=15, + blank=False, + default='Homepage', + help_text='z.B. Wikipedia-Eintrag' + ) + url = models.URLField( + verbose_name='URL', + blank=False, + help_text='Web-Adresse', + ) + + class Meta: + verbose_name = 'Autor-Website' + verbose_name_plural = 'Autor-Webseiten' + ordering = ['person', 'name',] + + list_display = ['person', 'name', 'url'] + + +class Contact(models.Model): + content_type = models.ForeignKey(ContentType, + on_delete = models.CASCADE, + help_text = 'Person oder Verlag', + default = Person, + blank=False,null=False, + ) + object_id = models.PositiveBigIntegerField( + default=1, + blank=False,null=False, + ) + content_object = GenericForeignKey("content_type", "object_id") name = models.CharField( verbose_name = 'Kontaktperson', blank=True, @@ -175,9 +206,12 @@ class Contact(models.Model): class Meta: verbose_name = 'Kontakt' verbose_name_plural = 'Kontakte' - ordering = ['reference', 'last_try'] + #ordering = ['content_object', 'last_try'] + indexes = [ + models.Index(fields=["content_type", "object_id"]), + ] - list_display = ['reference', 'last_try', 'user', 'success'] + list_display = ['content_object', 'name', 'last_try', 'user', 'success'] class Medium(models.Model): @@ -185,7 +219,6 @@ class Medium(models.Model): blank=False, on_delete=models.CASCADE, ) - # TODO: Medien in eigene Tabelle ausgliedern class IDs(models.TextChoices): Email = "Email", 'E-Mail' Phone = "Phone", 'Telefon' diff --git a/requirements.txt b/requirements.txt index 1a5c3a5..17642b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ django>6,<7 -Pillow - django-dotenv django-taggit django-taggit-helpers