WIP Quellen/Admin
This commit is contained in:
@@ -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
22
docs/README.md
Normal 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
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
django>6,<7
|
||||
Pillow
|
||||
|
||||
django-dotenv
|
||||
django-taggit
|
||||
django-taggit-helpers
|
||||
|
||||
Reference in New Issue
Block a user