WIP Quellen/Admin

This commit is contained in:
2026-01-25 21:50:21 +01:00
committed by Hraban Ramm
parent e48f9174c2
commit 7bf6587462
8 changed files with 177 additions and 34 deletions

View File

@@ -16,3 +16,4 @@ und für die öffentliche Rechercheplattform.
- Python 3.14 (funktioniert wahrscheinlich auch mit anderen Versionen) - Python 3.14 (funktioniert wahrscheinlich auch mit anderen Versionen)
- Django 6.0 - Django 6.0
- django-taggit - django-taggit
- django-taggit-helpers (uralt, funktioniert aber)

22
docs/README.md Normal file
View File

@@ -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

View File

@@ -5,6 +5,7 @@ see https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
import os import os
from pathlib import Path from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -36,6 +37,7 @@ ALLOWED_HOSTS = []
INSTALLED_APPS = [ INSTALLED_APPS = [
'quellen.apps.QuellenConfig', 'quellen.apps.QuellenConfig',
'taggit', 'taggit',
'taggit_helpers',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',

View File

@@ -13,7 +13,7 @@ def main():
if not os.path.isfile(env): if not os.path.isfile(env):
print("Configuration environment file (.env) not found!") print("Configuration environment file (.env) not found!")
sys.exit(1) sys.exit(1)
dotenv.load_dotenv(env) dotenv.read_dotenv(env)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'liederquelle.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'liederquelle.settings')
try: try:

View File

@@ -1,31 +1,62 @@
from django.contrib import admin 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 taggit_helpers.admin import TaggitListFilter, TaggitTabularInline
from django.contrib.contenttypes.admin import GenericTabularInline, GenericStackedInline
# TODO: Links zu Suchseiten mit Vorbelegungen # TODO: Links zu Suchseiten mit Vorbelegungen
class IdentityTabularInline(admin.TabularInline): class IdentityInline(admin.TabularInline):
model = Identity model = Identity
#ordering_field = ['slideshow', 'weight'] ordering_field = ['alias',]
ordering_field_hide_input = False min_num = 1
#fields = ['weight', 'display_title', 'target', 'image', 'overlay_image'] extra = 1
# exclude = ['display_content', 'overlay_tilt', 'overlay_class', 'activated'] 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) @admin.register(Person)
class PersonAdmin(admin.ModelAdmin): 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') search_fields = ('name', 'full_name', 'name_native', 'birth_name')
inlines = [ inlines = [
IdentityTabularInline IdentityInline,
AuthorIDInline,
AuthorURLInline,
ContactInline
] ]
@admin.register(Identity) @admin.register(Identity)
class IdentityAdmin(admin.ModelAdmin): class IdentityAdmin(admin.ModelAdmin):
list_display = ['person', 'author_id', 'alias', 'organization'] list_display = Identity.list_display
search_fields = ('alias', 'organization') search_fields = ('alias', 'organization')
list_filter = ['person', 'organization', TaggitListFilter] list_filter = ['person', 'organization', TaggitListFilter]
exclude = ('tags',) exclude = ('tags',)
@@ -36,19 +67,28 @@ class IdentityAdmin(admin.ModelAdmin):
@admin.register(AuthorID) @admin.register(AuthorID)
class AuthorIDAdmin(admin.ModelAdmin): class AuthorIDAdmin(admin.ModelAdmin):
list_display = ['person','key','value'] list_display = AuthorID.list_display
search_fields = ('value',) search_fields = ('value',)
@admin.register(AuthorURL)
class AuthorURLAdmin(admin.ModelAdmin):
list_display = AuthorURL.list_display
search_fields = ('url',)
@admin.register(Contact) @admin.register(Contact)
class ContactAdmin(admin.ModelAdmin): class ContactAdmin(admin.ModelAdmin):
date_hierarchy = 'last_try' date_hierarchy = 'last_try'
list_display = ['reference', 'name',] list_display = Contact.list_display
search_fields = ('name', 'address', 'remarks') search_fields = ('name', 'address', 'remarks')
#prepopulated_fields = {'user': ('',)} #prepopulated_fields = {'user': ('',)}
inlines = [
MediumInline,
]
@admin.register(Medium) @admin.register(Medium)
class MediumAdmin(admin.ModelAdmin): class MediumAdmin(admin.ModelAdmin):
list_display = ['contact','key','value'] list_display = Medium.list_display
search_fields = ('value',) search_fields = ('value',)

View File

@@ -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 django.db.models.deletion
import quellen.models
import taggit.managers import taggit.managers
from django.conf import settings from django.conf import settings
from django.db import migrations, models 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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('remarks', models.TextField(blank=True, verbose_name='Anmerkungen')),
], ],
options={
'verbose_name': 'Person',
'verbose_name_plural': 'Personen',
'ordering': ['name', 'birth_year'],
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Contact', name='Contact',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('address', models.TextField(blank=True, help_text='Postadresse', verbose_name='Adresse')),
('last_try', models.DateField(blank=True, null=True, verbose_name='letzter Kontaktversuch')), ('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?')), ('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')), ('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')), ('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 Projektmitarbeiter*in', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), ('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( migrations.CreateModel(
name='Medium', name='Medium',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('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')), ('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( migrations.CreateModel(
name='Identity', name='Identity',
@@ -57,9 +75,29 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('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')), ('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')), ('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( migrations.CreateModel(
name='AuthorID', 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')), ('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')), ('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'),
), ),
] ]

View File

@@ -1,6 +1,6 @@
from django.db import models from django.db import models
from taggit.managers import TaggableManager 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.contenttypes.models import ContentType
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -46,14 +46,14 @@ class Person(models.Model):
blank=True, blank=True,
#help_text='', #help_text='',
) )
# TODO: URLs wie Tags? contacts = GenericRelation('Contact')
class Meta: class Meta:
verbose_name = 'Person' verbose_name = 'Person'
verbose_name_plural = 'Personen' verbose_name_plural = 'Personen'
ordering = ['name','birth_year'] ordering = ['name','birth_year']
list_display = ['name'] list_display = ['name', 'full_name', 'birth_year']
def __str__(self): def __str__(self):
return self.name return self.name
@@ -92,7 +92,7 @@ class Identity(models.Model):
verbose_name_plural = 'Identitäten' verbose_name_plural = 'Identitäten'
ordering = ['author_id'] ordering = ['author_id']
list_display = ['author_id', 'alias'] list_display = ['person', 'author_id', 'alias', 'organization']
def __str__(self): def __str__(self):
return "{} ({})".format(self.author_id, self.alias) return "{} ({})".format(self.author_id, self.alias)
@@ -103,7 +103,6 @@ class AuthorID(models.Model):
blank=False, blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
# TODO: Medien in eigene Tabelle ausgliedern
class IDs(models.TextChoices): class IDs(models.TextChoices):
DISCOGS = "DISCOGS", 'Discogs' DISCOGS = "DISCOGS", 'Discogs'
GND = "GND", 'DNB GND' GND = "GND", 'DNB GND'
@@ -134,12 +133,44 @@ class AuthorID(models.Model):
list_display = ['person', 'key', 'value'] list_display = ['person', 'key', 'value']
class Contact(models.Model): class AuthorURL(models.Model):
reference = models.ForeignKey(ContentType, person = models.ForeignKey(Person,
on_delete = models.CASCADE, blank=False,
help_text = 'Person oder Verlag' on_delete=models.CASCADE,
# TODO: Das stimmt so nicht, ID des Objekts fehlt
) )
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( name = models.CharField(
verbose_name = 'Kontaktperson', verbose_name = 'Kontaktperson',
blank=True, blank=True,
@@ -175,9 +206,12 @@ class Contact(models.Model):
class Meta: class Meta:
verbose_name = 'Kontakt' verbose_name = 'Kontakt'
verbose_name_plural = 'Kontakte' 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): class Medium(models.Model):
@@ -185,7 +219,6 @@ class Medium(models.Model):
blank=False, blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
# TODO: Medien in eigene Tabelle ausgliedern
class IDs(models.TextChoices): class IDs(models.TextChoices):
Email = "Email", 'E-Mail' Email = "Email", 'E-Mail'
Phone = "Phone", 'Telefon' Phone = "Phone", 'Telefon'

View File

@@ -1,6 +1,4 @@
django>6,<7 django>6,<7
Pillow
django-dotenv django-dotenv
django-taggit django-taggit
django-taggit-helpers django-taggit-helpers