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)
- Django 6.0
- 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
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',

View File

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

View File

@@ -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',)

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 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'),
),
]

View File

@@ -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,
class AuthorURL(models.Model):
person = models.ForeignKey(Person,
blank=False,
on_delete=models.CASCADE,
help_text = 'Person oder Verlag'
# 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(
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'

View File

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