From c8d06b60ec41cb3a75096ad64e7c3d8a6f9d0743 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Sun, 11 Feb 2024 09:19:00 +0100
Subject: [PATCH 01/19] mgm cmd to serialize relations added
---
.gitignore | 1 +
.../management/commands/__init__.py | 0
.../management/commands/dump_relations.py | 37 +++++++++++++++++++
apis_core/apis_relations/models.py | 25 ++++---------
4 files changed, 45 insertions(+), 18 deletions(-)
create mode 100644 apis_core/apis_relations/management/commands/__init__.py
create mode 100644 apis_core/apis_relations/management/commands/dump_relations.py
diff --git a/.gitignore b/.gitignore
index c23d6a3..c0aaae0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,3 +180,4 @@ hansi.csv
media/duplicated_*.csv
Untitled.ipynb
listevent.xml
+relations.csv
diff --git a/apis_core/apis_relations/management/commands/__init__.py b/apis_core/apis_relations/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
new file mode 100644
index 0000000..bebb4b3
--- /dev/null
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -0,0 +1,37 @@
+import os
+import pandas as pd
+from datetime import datetime
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from tqdm import tqdm
+from typing import Any
+from apis_core.apis_relations.models import AbstractRelation
+from dumper.utils import write_report
+
+
+class Command(BaseCommand):
+ help = "Dumps all relations into a csv"
+
+ def handle(self, *args: Any, **options: Any) -> str | None:
+ start_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
+ print("dumping all relations into a csv")
+
+ data = []
+ issues = []
+ for x in AbstractRelation.get_all_relation_classes():
+ print(x.__name__)
+ for y in tqdm(x.objects.all()):
+ try:
+ data.append(y.get_web_object())
+ except AttributeError:
+ issues.append(y)
+ df = pd.DataFrame(data)
+ save_path = os.path.join(settings.MEDIA_ROOT, "relations.csv")
+ df.to_csv(save_path, index=False)
+ end_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
+ report = [os.path.basename(__file__), start_time, end_time]
+ write_report(report)
+ print(f"serialized {len(df)} relations")
+ return "done"
diff --git a/apis_core/apis_relations/models.py b/apis_core/apis_relations/models.py
index 851ff09..0c2e12e 100644
--- a/apis_core/apis_relations/models.py
+++ b/apis_core/apis_relations/models.py
@@ -1,6 +1,7 @@
import inspect
import sys
+from icecream import ic
from apis_core.apis_entities.models import Person
from apis_core.apis_metainfo.models import TempEntityClass
@@ -45,28 +46,16 @@ def __str__(self):
)
def get_web_object(self):
- namea = self.get_related_entity_instancea().name
- nameb = self.get_related_entity_instanceb().name
-
- if self.get_related_entity_classa() == Person:
- namea += ", "
- if self.get_related_entity_instancea().first_name is None:
- namea += "-"
- else:
- namea += self.get_related_entity_instancea().first_name
-
- if self.get_related_entity_classb() == Person:
- nameb += ", "
- if self.get_related_entity_instanceb().first_name is None:
- nameb += "-"
- else:
- nameb += self.get_related_entity_instanceb().first_name
+ namea = self.get_related_entity_instancea()
+ nameb = self.get_related_entity_instanceb()
result = {
"relation_pk": self.pk,
"relation_type": self.relation_type.name,
- self.get_related_entity_field_namea(): namea,
- self.get_related_entity_field_nameb(): nameb,
+ "source": namea,
+ "target": nameb,
+ "source_id": namea.id,
+ "target_id": nameb.id,
"start_date": self.start_date_written,
"end_date": self.end_date_written,
}
From 4869a0d36d0a5032b1a47d0bcb385bfb6c9d6b69 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 12 Feb 2024 15:39:45 +0100
Subject: [PATCH 02/19] mgm cmd to find delete duplicated relations added
---
.../management/commands/dump_relations.py | 41 +++++++++++++++++--
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
index bebb4b3..52979f8 100644
--- a/apis_core/apis_relations/management/commands/dump_relations.py
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -1,14 +1,16 @@
import os
import pandas as pd
+import recordlinkage
+
from datetime import datetime
from django.conf import settings
from django.core.management.base import BaseCommand
-
+from icecream import ic
from tqdm import tqdm
from typing import Any
from apis_core.apis_relations.models import AbstractRelation
-from dumper.utils import write_report
+from dumper.utils import upload_files_to_owncloud, write_report
class Command(BaseCommand):
@@ -24,14 +26,47 @@ def handle(self, *args: Any, **options: Any) -> str | None:
print(x.__name__)
for y in tqdm(x.objects.all()):
try:
- data.append(y.get_web_object())
+ item = y.get_web_object()
+ item["relation_pk"] = y.id
+ data.append(item)
except AttributeError:
issues.append(y)
df = pd.DataFrame(data)
+ print("lets find and delete duplicated relations")
+ df.set_index("relation_pk", inplace=True, drop=False)
+ save_path = os.path.join(settings.MEDIA_ROOT, "relations.csv")
+ print(f"serialized {len(df)} relations")
+ df.to_csv(save_path, index=False)
+
+ df = pd.read_csv(save_path).fillna("nodate")
+ df.set_index("relation_pk", inplace=True, drop=False)
+ indexer = recordlinkage.Index()
+ indexer.block(["relation_type", "source_id", "target_id", "start_date", "end_date"])
+ duplicates = indexer.index(df)
+ print(f"deleting {len(duplicates)} duplicated relations")
+
+ deleted = []
+ for double in duplicates:
+ for x in AbstractRelation.get_all_relation_classes():
+ try:
+ item = x.objects.get(id=double[1])
+ except: # noqa
+ continue
+ deleted.append(item.id)
+ item.delete()
+ break
+ print(deleted)
+ df.drop(deleted)
save_path = os.path.join(settings.MEDIA_ROOT, "relations.csv")
df.to_csv(save_path, index=False)
end_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
report = [os.path.basename(__file__), start_time, end_time]
write_report(report)
print(f"serialized {len(df)} relations")
+ files = list()
+ files.append(save_path)
+ try:
+ upload_files_to_owncloud(files)
+ except Exception as e:
+ ic(e)
return "done"
From af381518c6d263d022d7f9db4543764ef6b06f67 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 12 Feb 2024 16:08:21 +0100
Subject: [PATCH 03/19] h1 -> h2; allow link to headline and closes #146
---
dumper/templates/dumper/about.html | 116 +++++++++++++++++------------
static/css/style.css | 9 +++
2 files changed, 79 insertions(+), 46 deletions(-)
diff --git a/dumper/templates/dumper/about.html b/dumper/templates/dumper/about.html
index 3df4eb6..58d17d2 100644
--- a/dumper/templates/dumper/about.html
+++ b/dumper/templates/dumper/about.html
@@ -4,31 +4,37 @@
{% block title %}Über das Projekt{% endblock %}
{% block content %}
-
+
PMB
-
Personen der Moderne Basis*
+
Personen der Moderne Basis*
Die PMB ist ein Webservice für Personen, Werke, Institutionen, Orte und Ereignisse speziell
für die Zeit in Wien um 1900. Ihre einfachste Funktion besteht darin, für verschiedene
Projekte eine gemeinsame Datenbank zu schaffen. Das erleichtert einerseits die Erfassung von
Personen, wenn diese bereits angelegt sind. Es vereinfacht andererseits die Suche über
mehrere Projekte hinweg. Verweise auf andere Normdatensätze (GND, geonames.org) erlauben den
automatisierten Abgleich, ohne Fehler von diesen übernehmen zu müssen.
-
Datenherkunft
+
Datenherkunft#
Größere Bestände und der Zeitpunkt ihrer Aufnahme
Peter Michael Braunwarth: Personen-, Werk- und Ortindex: Arthur Schnitzler: Tagebuch
1879–1931. Herausgegeben von der Kommission für literarische Gebrauchsformen der
Österreichischen Akademie der Wissenschaften, Obmann: Werner Welzig. Wien: Verlag der
- Österreichischen Akademie der Wissenschaften, 1981–2000.
- 10.772 Entitäten (Februar 2019)
+ Österreichischen Akademie der Wissenschaften, 1981–2000.
+ 10.772 Entitäten (Februar 2019)
+
Martin Anton Müller: Personen-, Werk-, Organisations- und Ortindex für die Edition:
Arthur Schnitzler – Briefwechsel mit Autorinnen und Autoren (2018–2021). Diese sind eine
Weiterführung von den von Kurt Ifkovits und Martin Anton Müller erstellten Daten zu:
Hermann Bahr, Arthur Schnitzler: Briefwechsel, Aufzeichnungen, Dokumente. Hg. K. I. und
- M. A. M. Göttingen: Wallstein 2018.
- 6.755 Entitäten (Februar 2019)
-Martin Anton Müller: Hermann Bahr – Textverzeichnis. Weimar: VDG 2014rund 7.000 Entitäten (März 2023)
-Hermann Bahr: Tagebücher, Skizzenbücher, Notizhefte. Hg. Moritz Csáky, Mitarbeit von Lottelis Moser, Helene Zand, Lukas Mayerhofer und Kurt Ifkovits. Wien, Köln, Weimar: Böhlau 1994-2003, Link (Januar 2024)
+ M. A. M. Göttingen: Wallstein 2018.
+ 6.755 Entitäten (Februar 2019)
+
+ Martin Anton Müller: Hermann Bahr – Textverzeichnis. Weimar: VDG 2014rund 7.000 Entitäten (März
+ 2023)
+ Hermann Bahr: Tagebücher, Skizzenbücher, Notizhefte. Hg. Moritz Csáky, Mitarbeit von Lottelis Moser, Helene
+ Zand, Lukas Mayerhofer und Kurt Ifkovits. Wien, Köln, Weimar: Böhlau 1994-2003, Link (Januar 2024)
Die Entitäten werden kontinuierlich erweitert, durchgesehen und auch miteinander in Beziehung
gesetzt.
@@ -37,33 +43,35 @@
Datenherkunft
Arthur Schnitzler – Briefwechsel mit Autorinnen und Autoren, link (2018–[2024])
Intertextuality in the Legal Papers of Karl Kraus – A Scholarly Digital Edition, link (2019–2022)
+ href="https://www.kraus.wienbibliothek.at">link (2019–2022)
* Der Name ist ein Sonderfall eines Akronyms. Er versteht sich als Referenz auf Peter
Michael Braunwarth, dessen Arbeit den Grundstock bildet.
-
-
Open Source
-
Alle Daten sind mit der Lizenz CC BY 4.0 versehen und können ohne Rücksprache verwendet und weiter
- bearbeitet werden. Als Listen können sie zur Gänze geladen werden (rechte Maustaste: »speichern unter«):
-
- Personen .
+
+ Open Source#
+ Alle Daten sind mit der Lizenz CC BY 4.0
+ versehen und können ohne Rücksprache verwendet und weiter
+ bearbeitet werden. Als Listen können sie zur Gänze geladen werden (rechte Maustaste: »speichern unter«):
+
+
-
Einzelne Einträge können über die API (die JSON- oder TEI-Symbole bei einem Eintrag) geladen werden.
-
Wir freuen uns, wenn sich weitere Unternehmungen anschließen und die PMB für die Erfassung ihrer Entitäten verwenden würden
- und bitten um Kontaktaufnahme.
-
Richtlinien
+
Einzelne Einträge können über die API (die JSON- oder TEI-Symbole bei einem Eintrag) geladen werden.
+
Wir freuen uns, wenn sich weitere Unternehmungen anschließen und die PMB für die Erfassung ihrer Entitäten
+ verwenden würden
+ und bitten um Kontaktaufnahme.
+
Richtlinien#
Die Herkunft der Anfangsdaten brachten als Erbe einige Uneinheitlichkeiten mit sich. Im
Folgenden werden die unterschiedlichen Funktionen beschrieben und skizziert, wie die
Einträge möglichst regelkonform angelegt werden.
-
Personen
+
Personen#
Zumindest das grundsätzliche Verständnis, was eine Person ist, dürfte zu wenig Diskussion
führen. Die relevante Unterscheidung: Die PMB verzeichnet real existierende Personen und
solche, von denen angenommen werden kann, dass sie existierten (»Homer«), aber keine
@@ -177,12 +185,20 @@
Von Person zu Person
- ist adoptiertes Elternteil von
+
+
+ ist adoptiertes Elternteil von
+
+
-
+
+
+ ist Stiefelternteil von
+
+
@@ -222,7 +238,11 @@ Von Person zu Person
@@ -298,7 +318,7 @@ Labels
Die verschiedenen Labels sind über das Dropdown-Menü auszuwählen und weitgehend
selbsterklärend. Sie enthalten bei Personen vor allem Namensvarianten wie Geburtsname,
Pseudonym, Rufname.
- Werke
+ Werke#
Werke werden nach Möglichkeit in der ersten Fassung aufgenommen (»Lieutenant Gustl«), weitere
Namensvarianten (»Leutnant Gustl«) können als Label erfasst werden. Ein Werk und seine
Übersetzung werden im Normalfall als zwei Werke geführt, die beiden durch eine Relation
@@ -395,8 +415,8 @@
Von Werk zu Institution
enthält
enthalten in
-
-
Institutionen
+
+
Institutionen#
Als Institutionen werden juristische, politische oder anderweitige Einrichtungen erfasst,
deren Handlungsraum nicht genau lokalisierbar sind. Dazu gehören neben Unternehmen auch
Verlage und Literaturpreise, da ihre Produkte nicht an einem bestimmten Ort rezipiert
@@ -420,15 +440,17 @@
Von einer Institution zu einem Ort
-
Orte
+
Orte#
Orte sind im einfachsten Sinne gemeint, als Stellen, zu denen man physisch hingehen kann.
Anders gesagt: Gibt es einen Längen- und einen Breitengrad, ist es ein Ort. Das umfasst also auch
Organisationen wie Theater – die man physisch aufsucht.
Ortstypen
Ein Teil der Orte sind mit einem eigenen Vokabular gekennzeichnet. Dieses findet sich hier:
- https://vocabs.acdh-dev.oeaw.ac.at/pmbplacetypes/de/?clang=de . Die meisten Orte
- sind aber nach Geonames klassifiziert.
+
https://vocabs.acdh-dev.oeaw.ac.at/pmbplacetypes/de/?clang=de .
+ Die meisten Orte
+ sind aber nach Geonames klassifiziert.
+
Für Orte sollte nach Möglichkeit eine Geonames-URI vorhanden sein.
Beziehungen
Beziehungen von Ort zu Ort
@@ -436,20 +458,22 @@
Beziehungen von Ort zu Ort
gehört zu
enthält
-
Wichtige Ressourcen
+
Wichtige Ressourcen#
URIs
URIs sind beständige, eindeutige Marker, mit denen sich Verbindungen zu
- anderen Projekten herstellen lassen. Dieses Projekt verwendet hauptsächlich drei davon:
+ anderen Projekten herstellen lassen. Dieses Projekt verwendet hauptsächlich drei davon:
-
Eine Konkordanz zwischen PMB-URIs und GND-URIs kann über folgende BEACON Datei bezogen werden.
-
Eine Konkordanz zwischen PMB-URIs und Wikidata-URIs kann über folgende BEACON Datei bezogen werden.
+
Eine Konkordanz zwischen PMB-URIs und GND-URIs kann über folgende BEACON Datei bezogen werden.
+
Eine Konkordanz zwischen PMB-URIs und Wikidata-URIs kann über folgende BEACON Datei bezogen werden.
Wikipedia-Einträge werden nicht direkt verlinkt. Auf der linken Seite im Menü eines Wikipedia-Eintrags
- steht ein Link auf das entsprechende Wikidata-Objekt.
-
+ steht ein Link auf das entsprechende Wikidata-Objekt.
+
Zur Personensuche
Wien
@@ -461,8 +485,8 @@ Wien
Jüdische Matrikeln
(Kostenfreie Registrierung. Achtung: Nicht auf die Datenbank setzen, sondern die Bilder
ansehen. Einträge vor allem in Wien-Allgemein und Wien I.)
- Lehmann online OCR (Adressbuch Wien. Wenn die Volltexterkennung nicht
+ Lehmann online OCR
+ (Adressbuch Wien. Wenn die Volltexterkennung nicht
funktioniert, ist die herkömmliche Blätterversion besser).
ANNO Zeitungen (Grade für Namen empfiehlt sich die
Abstandsuche: "Johann Goethe"~2 findet auch "Johann Wolfgang Goethe")
@@ -475,5 +499,5 @@ Berlin
The European Library
(Internationale Zeitungen, auch Berlin.)
-
+
{% endblock %}
\ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
index ea38e23..42a830a 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -109,4 +109,13 @@ main {
.table-relations th
{
width: 25% !important;
+}
+
+.anchor-link {
+ padding: 0 .175rem;
+ font-weight: 400;
+ color: rgba(13,110,253,0.5);
+ text-decoration: none;
+ opacity: 0;
+ transition: color 0.15s ease-in-out,opacity 0.15s ease-in-out;
}
\ No newline at end of file
From 5e7d797b3ccddd6c3b843c9dee2330a7500c67f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20Anton=20M=C3=BCller?=
Date: Mon, 12 Feb 2024 13:44:13 +0100
Subject: [PATCH 04/19] imprint params in redmine nachtgetragen
---
dumper/templates/dumper/index.html | 4 ++--
templates/partials/footer.html | 5 ++++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/dumper/templates/dumper/index.html b/dumper/templates/dumper/index.html
index c2ab7ca..b51eb5e 100644
--- a/dumper/templates/dumper/index.html
+++ b/dumper/templates/dumper/index.html
@@ -49,8 +49,8 @@ {{ place_count|intcomma }} Orte
{{ work_count|intcomma }} Werke
- Literarische und künstlerische Kunstwerke, einschließlich
- Zeitungen und Zeitschriften, die bislang erfasst sind. Fast
+
Literarische und künstlerische Schöpfungen, einschließlich
+ Zeitungen und Zeitschriften. Fast
ein Drittel der Werke stammt von Hermann Bahr
diff --git a/templates/partials/footer.html b/templates/partials/footer.html
index 33bf83b..80c06fe 100644
--- a/templates/partials/footer.html
+++ b/templates/partials/footer.html
@@ -1,6 +1,9 @@
{% load static %}
+
From 477db019a44073e6caa7fec0822b156731216c67 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 12 Feb 2024 16:08:21 +0100
Subject: [PATCH 05/19] h1 -> h2; allow link to headline and closes #146
---
dumper/templates/dumper/about.html | 116 +++++++++++++++++------------
static/css/style.css | 9 +++
2 files changed, 79 insertions(+), 46 deletions(-)
diff --git a/dumper/templates/dumper/about.html b/dumper/templates/dumper/about.html
index 3df4eb6..58d17d2 100644
--- a/dumper/templates/dumper/about.html
+++ b/dumper/templates/dumper/about.html
@@ -4,31 +4,37 @@
{% block title %}Über das Projekt{% endblock %}
{% block content %}
-
+
PMB
-
Personen der Moderne Basis*
+
Personen der Moderne Basis*
Die PMB ist ein Webservice für Personen, Werke, Institutionen, Orte und Ereignisse speziell
für die Zeit in Wien um 1900. Ihre einfachste Funktion besteht darin, für verschiedene
Projekte eine gemeinsame Datenbank zu schaffen. Das erleichtert einerseits die Erfassung von
Personen, wenn diese bereits angelegt sind. Es vereinfacht andererseits die Suche über
mehrere Projekte hinweg. Verweise auf andere Normdatensätze (GND, geonames.org) erlauben den
automatisierten Abgleich, ohne Fehler von diesen übernehmen zu müssen.
-
Datenherkunft
+
Datenherkunft#
Größere Bestände und der Zeitpunkt ihrer Aufnahme
Peter Michael Braunwarth: Personen-, Werk- und Ortindex: Arthur Schnitzler: Tagebuch
1879–1931. Herausgegeben von der Kommission für literarische Gebrauchsformen der
Österreichischen Akademie der Wissenschaften, Obmann: Werner Welzig. Wien: Verlag der
- Österreichischen Akademie der Wissenschaften, 1981–2000.
- 10.772 Entitäten (Februar 2019)
+ Österreichischen Akademie der Wissenschaften, 1981–2000.
+ 10.772 Entitäten (Februar 2019)
+
Martin Anton Müller: Personen-, Werk-, Organisations- und Ortindex für die Edition:
Arthur Schnitzler – Briefwechsel mit Autorinnen und Autoren (2018–2021). Diese sind eine
Weiterführung von den von Kurt Ifkovits und Martin Anton Müller erstellten Daten zu:
Hermann Bahr, Arthur Schnitzler: Briefwechsel, Aufzeichnungen, Dokumente. Hg. K. I. und
- M. A. M. Göttingen: Wallstein 2018.
- 6.755 Entitäten (Februar 2019)
-Martin Anton Müller: Hermann Bahr – Textverzeichnis. Weimar: VDG 2014rund 7.000 Entitäten (März 2023)
-Hermann Bahr: Tagebücher, Skizzenbücher, Notizhefte. Hg. Moritz Csáky, Mitarbeit von Lottelis Moser, Helene Zand, Lukas Mayerhofer und Kurt Ifkovits. Wien, Köln, Weimar: Böhlau 1994-2003, Link (Januar 2024)
+ M. A. M. Göttingen: Wallstein 2018.
+ 6.755 Entitäten (Februar 2019)
+
+ Martin Anton Müller: Hermann Bahr – Textverzeichnis. Weimar: VDG 2014rund 7.000 Entitäten (März
+ 2023)
+ Hermann Bahr: Tagebücher, Skizzenbücher, Notizhefte. Hg. Moritz Csáky, Mitarbeit von Lottelis Moser, Helene
+ Zand, Lukas Mayerhofer und Kurt Ifkovits. Wien, Köln, Weimar: Böhlau 1994-2003, Link (Januar 2024)
Die Entitäten werden kontinuierlich erweitert, durchgesehen und auch miteinander in Beziehung
gesetzt.
@@ -37,33 +43,35 @@
Datenherkunft
Arthur Schnitzler – Briefwechsel mit Autorinnen und Autoren, link (2018–[2024])
Intertextuality in the Legal Papers of Karl Kraus – A Scholarly Digital Edition, link (2019–2022)
+ href="https://www.kraus.wienbibliothek.at">link (2019–2022)
* Der Name ist ein Sonderfall eines Akronyms. Er versteht sich als Referenz auf Peter
Michael Braunwarth, dessen Arbeit den Grundstock bildet.
-
-
Open Source
-
Alle Daten sind mit der Lizenz CC BY 4.0 versehen und können ohne Rücksprache verwendet und weiter
- bearbeitet werden. Als Listen können sie zur Gänze geladen werden (rechte Maustaste: »speichern unter«):
-
- Personen .
+
+ Open Source#
+ Alle Daten sind mit der Lizenz CC BY 4.0
+ versehen und können ohne Rücksprache verwendet und weiter
+ bearbeitet werden. Als Listen können sie zur Gänze geladen werden (rechte Maustaste: »speichern unter«):
+
+
-
Einzelne Einträge können über die API (die JSON- oder TEI-Symbole bei einem Eintrag) geladen werden.
-
Wir freuen uns, wenn sich weitere Unternehmungen anschließen und die PMB für die Erfassung ihrer Entitäten verwenden würden
- und bitten um Kontaktaufnahme.
-
Richtlinien
+
Einzelne Einträge können über die API (die JSON- oder TEI-Symbole bei einem Eintrag) geladen werden.
+
Wir freuen uns, wenn sich weitere Unternehmungen anschließen und die PMB für die Erfassung ihrer Entitäten
+ verwenden würden
+ und bitten um Kontaktaufnahme.
+
Richtlinien#
Die Herkunft der Anfangsdaten brachten als Erbe einige Uneinheitlichkeiten mit sich. Im
Folgenden werden die unterschiedlichen Funktionen beschrieben und skizziert, wie die
Einträge möglichst regelkonform angelegt werden.
-
Personen
+
Personen#
Zumindest das grundsätzliche Verständnis, was eine Person ist, dürfte zu wenig Diskussion
führen. Die relevante Unterscheidung: Die PMB verzeichnet real existierende Personen und
solche, von denen angenommen werden kann, dass sie existierten (»Homer«), aber keine
@@ -177,12 +185,20 @@
Von Person zu Person
- ist adoptiertes Elternteil von
+
+
+ ist adoptiertes Elternteil von
+
+
-
+
+
+ ist Stiefelternteil von
+
+
@@ -222,7 +238,11 @@ Von Person zu Person
@@ -298,7 +318,7 @@ Labels
Die verschiedenen Labels sind über das Dropdown-Menü auszuwählen und weitgehend
selbsterklärend. Sie enthalten bei Personen vor allem Namensvarianten wie Geburtsname,
Pseudonym, Rufname.
- Werke
+ Werke#
Werke werden nach Möglichkeit in der ersten Fassung aufgenommen (»Lieutenant Gustl«), weitere
Namensvarianten (»Leutnant Gustl«) können als Label erfasst werden. Ein Werk und seine
Übersetzung werden im Normalfall als zwei Werke geführt, die beiden durch eine Relation
@@ -395,8 +415,8 @@
Von Werk zu Institution
enthält
enthalten in
-
-
Institutionen
+
+
Institutionen#
Als Institutionen werden juristische, politische oder anderweitige Einrichtungen erfasst,
deren Handlungsraum nicht genau lokalisierbar sind. Dazu gehören neben Unternehmen auch
Verlage und Literaturpreise, da ihre Produkte nicht an einem bestimmten Ort rezipiert
@@ -420,15 +440,17 @@
Von einer Institution zu einem Ort
-
Orte
+
Orte#
Orte sind im einfachsten Sinne gemeint, als Stellen, zu denen man physisch hingehen kann.
Anders gesagt: Gibt es einen Längen- und einen Breitengrad, ist es ein Ort. Das umfasst also auch
Organisationen wie Theater – die man physisch aufsucht.
Ortstypen
Ein Teil der Orte sind mit einem eigenen Vokabular gekennzeichnet. Dieses findet sich hier:
- https://vocabs.acdh-dev.oeaw.ac.at/pmbplacetypes/de/?clang=de . Die meisten Orte
- sind aber nach Geonames klassifiziert.
+
https://vocabs.acdh-dev.oeaw.ac.at/pmbplacetypes/de/?clang=de .
+ Die meisten Orte
+ sind aber nach Geonames klassifiziert.
+
Für Orte sollte nach Möglichkeit eine Geonames-URI vorhanden sein.
Beziehungen
Beziehungen von Ort zu Ort
@@ -436,20 +458,22 @@
Beziehungen von Ort zu Ort
gehört zu
enthält
-
Wichtige Ressourcen
+
Wichtige Ressourcen#
URIs
URIs sind beständige, eindeutige Marker, mit denen sich Verbindungen zu
- anderen Projekten herstellen lassen. Dieses Projekt verwendet hauptsächlich drei davon:
+ anderen Projekten herstellen lassen. Dieses Projekt verwendet hauptsächlich drei davon:
-
Eine Konkordanz zwischen PMB-URIs und GND-URIs kann über folgende BEACON Datei bezogen werden.
-
Eine Konkordanz zwischen PMB-URIs und Wikidata-URIs kann über folgende BEACON Datei bezogen werden.
+
Eine Konkordanz zwischen PMB-URIs und GND-URIs kann über folgende BEACON Datei bezogen werden.
+
Eine Konkordanz zwischen PMB-URIs und Wikidata-URIs kann über folgende BEACON Datei bezogen werden.
Wikipedia-Einträge werden nicht direkt verlinkt. Auf der linken Seite im Menü eines Wikipedia-Eintrags
- steht ein Link auf das entsprechende Wikidata-Objekt.
-
+ steht ein Link auf das entsprechende Wikidata-Objekt.
+
Zur Personensuche
Wien
@@ -461,8 +485,8 @@ Wien
Jüdische Matrikeln
(Kostenfreie Registrierung. Achtung: Nicht auf die Datenbank setzen, sondern die Bilder
ansehen. Einträge vor allem in Wien-Allgemein und Wien I.)
- Lehmann online OCR (Adressbuch Wien. Wenn die Volltexterkennung nicht
+ Lehmann online OCR
+ (Adressbuch Wien. Wenn die Volltexterkennung nicht
funktioniert, ist die herkömmliche Blätterversion besser).
ANNO Zeitungen (Grade für Namen empfiehlt sich die
Abstandsuche: "Johann Goethe"~2 findet auch "Johann Wolfgang Goethe")
@@ -475,5 +499,5 @@ Berlin
The European Library
(Internationale Zeitungen, auch Berlin.)
-
+
{% endblock %}
\ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
index ea38e23..42a830a 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -109,4 +109,13 @@ main {
.table-relations th
{
width: 25% !important;
+}
+
+.anchor-link {
+ padding: 0 .175rem;
+ font-weight: 400;
+ color: rgba(13,110,253,0.5);
+ text-decoration: none;
+ opacity: 0;
+ transition: color 0.15s ease-in-out,opacity 0.15s ease-in-out;
}
\ No newline at end of file
From 0ae9e94d049aa8f953c961847e9f68e4557b3635 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Wed, 14 Feb 2024 17:03:05 +0100
Subject: [PATCH 06/19] compare coordinates for duplicates places, see
https://github.com/arthur-schnitzler/pmb-service/issues/147
---
.../management/commands/find_duplicated_places.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/apis_core/apis_entities/management/commands/find_duplicated_places.py b/apis_core/apis_entities/management/commands/find_duplicated_places.py
index c4f7add..0742aea 100644
--- a/apis_core/apis_entities/management/commands/find_duplicated_places.py
+++ b/apis_core/apis_entities/management/commands/find_duplicated_places.py
@@ -1,6 +1,7 @@
import os
import pandas as pd
import recordlinkage
+from recordlinkage.compare import Geographic
from typing import Any
from django.conf import settings
@@ -18,11 +19,13 @@ def handle(self, *args: Any, **options: Any) -> str | None:
props = [
"id",
"name",
+ "lat",
+ "lng"
]
df = pd.DataFrame(
Place.objects.values_list(*props),
columns=props,
- ).astype("str")
+ ).astype("str").fillna("nix")
df["custom_index"] = df["id"].astype(str) + " " + df["name"]
df.set_index("custom_index", inplace=True)
indexer = recordlinkage.Index()
@@ -31,8 +34,10 @@ def handle(self, *args: Any, **options: Any) -> str | None:
len(candidate_links)
compare_cl = recordlinkage.Compare()
compare_cl.exact("name", "name", label="name")
+ compare_cl.exact("lat", "lat", label="lat")
+ compare_cl.exact("lng", "lng", label="lng")
features = compare_cl.compute(candidate_links, df)
- matches = features[features.sum(axis=1) > 0]
+ matches = features[features.sum(axis=1) > 2]
save_path = os.path.join(settings.MEDIA_ROOT, "duplicated_places.csv")
matches.to_csv(save_path)
print(f"found {len(matches)} potential duplicates")
From 07a9913c10d7dfa0f1789b491109d2092eedc6e9 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Thu, 15 Feb 2024 15:41:01 +0100
Subject: [PATCH 07/19] no more duplicated relations
---
apis_core/apis_relations/forms2.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/apis_core/apis_relations/forms2.py b/apis_core/apis_relations/forms2.py
index 7582b8c..f4529fd 100644
--- a/apis_core/apis_relations/forms2.py
+++ b/apis_core/apis_relations/forms2.py
@@ -73,8 +73,19 @@ def save(self, site_instance, instance=None, commit=True):
target = AbstractEntity.get_entity_class_of_name(self.rel_accessor[0])
t1 = target.get_or_create_uri(cd["target"])
setattr(x, self.rel_accessor[2], t1)
+ params = {
+ self.rel_accessor[3]: site_instance,
+ self.rel_accessor[2]: t1,
+ "start_date_written": cd["start_date_written"],
+ "end_date_written": cd["end_date_written"],
+ "relation_type_id": cd["relation_type"],
+ }
if commit:
- x.save()
+ qs = x.__class__.objects.filter(**params)
+ if qs.count() > 0:
+ pass
+ else:
+ x.save()
return x
def get_text_id(self):
From 97a2892562dcc0c3bf267a607f8a4db6b11f9103 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Thu, 15 Feb 2024 15:46:31 +0100
Subject: [PATCH 08/19] more cronjobs
---
crontab | 3 +++
1 file changed, 3 insertions(+)
diff --git a/crontab b/crontab
index d19e48a..4fb5aa1 100644
--- a/crontab
+++ b/crontab
@@ -5,6 +5,9 @@
1 3 * * * root cd /opt/app && /usr/local/bin/python3 manage.py wikipedia_minter >> /var/log/cron.log 2>&1
30 3 * * * root cd /opt/app && /usr/local/bin/python3 manage.py wikidata_minter >> /var/log/cron.log 2>&1
1 4 * * * root cd /opt/app && /usr/local/bin/python3 manage.py dump_entities >> /var/log/cron.log 2>&1
+30 5 * * * root cd /opt/app && /usr/local/bin/python3 manage.py dump_relations >> /var/log/cron.log 2>&1
1 6 * * * root cd /opt/app && /usr/local/bin/python3 manage.py add_gn_feature_codes >> /var/log/cron.log 2>&1
1 7 * * * root cd /opt/app && /usr/local/bin/python3 manage.py fetch_images >> /var/log/cron.log 2>&1
+30 7 * * * root cd /opt/app && /usr/local/bin/python3 manage.py find_duplicted_persons >> /var/log/cron.log 2>&1
+50 7 * * * root cd /opt/app && /usr/local/bin/python3 manage.py find_duplicted_places >> /var/log/cron.log 2>&1
#
From 84b5e98bc0e6851ab03f7b251f2c75777b1e7e82 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Thu, 15 Feb 2024 15:55:50 +0100
Subject: [PATCH 09/19] updated codecov action [skip ci]
---
.github/workflows/test.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fa03400..3f3fc24 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -41,7 +41,7 @@ jobs:
- name: Create Coverage Report
run: coverage xml
- name: "Upload coverage to Codecov"
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v4
with:
token: ${{secrets.CODECOV_TOKEN}}
file: ./coverage.xml
From 73d6d37818513ac717ed89f9d19b7fc7015cda09 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Thu, 15 Feb 2024 16:00:27 +0100
Subject: [PATCH 10/19] closes #152
---
apis_core/apis_entities/list_view_work.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apis_core/apis_entities/list_view_work.py b/apis_core/apis_entities/list_view_work.py
index 8e4ce4e..a00a376 100644
--- a/apis_core/apis_entities/list_view_work.py
+++ b/apis_core/apis_entities/list_view_work.py
@@ -49,9 +49,9 @@
class WorkListFilter(MyBaseFilter):
name = django_filters.CharFilter(
- lookup_expr="icontains",
+ method="name_label_filter",
label="Werktitel",
- help_text="eingegebene Zeichenkette muss im Titel enthalten sein",
+ help_text="eingegebene Zeichenkette muss im Titel oder in einem der Labels enthalten sein",
)
references = django_filters.CharFilter(
lookup_expr="icontains",
From bcd42994442aecf56fcd0878edda1981c5035980 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Thu, 15 Feb 2024 16:21:38 +0100
Subject: [PATCH 11/19] closes #143
---
issue__80_professions.ipynb | 78 ++++++++++++++-----------------------
1 file changed, 29 insertions(+), 49 deletions(-)
diff --git a/issue__80_professions.ipynb b/issue__80_professions.ipynb
index dcc752e..875c26f 100644
--- a/issue__80_professions.ipynb
+++ b/issue__80_professions.ipynb
@@ -8,9 +8,17 @@
"# run 2024-01-19 on production server"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "014098af",
+ "metadata": {},
+ "source": [
+ "## run again 2024-02-15"
+ ]
+ },
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": null,
"id": "2fe08400",
"metadata": {},
"outputs": [],
@@ -21,52 +29,22 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"id": "2b4ee9e3",
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "200\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"df = gsheet_to_df(\"1MnS_eJbPNLzXp4YkS5I9Xkhv2GVPbYtrFiQjJla5rJE\")"
]
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": null,
"id": "f47bcd68",
"metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "1518it [01:10, 27.09it/s]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "1513\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "1567it [01:12, 21.47it/s]\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
- "for i, row in tqdm(df.iterrows()):\n",
+ "for i, row in tqdm(df.iterrows(), total=len(df)):\n",
" try:\n",
" pl = ProfessionType.objects.get(id=row[\"id\"])\n",
" except:\n",
@@ -79,21 +57,13 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": null,
"id": "f820cd7f",
"metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "1567it [01:34, 16.66it/s] \n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"delete_us = []\n",
- "for i, row in tqdm(df.iterrows()):\n",
+ "for i, row in tqdm(df.iterrows(), total=len(df)):\n",
" if isinstance(row[\"duplicates\"], str):\n",
" pl = ProfessionType.objects.get(id=row[\"id\"])\n",
" ids = [int(x) for x in row[\"duplicates\"].split('|')]\n",
@@ -112,12 +82,12 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": null,
"id": "fe06fca6",
"metadata": {},
"outputs": [],
"source": [
- "for x in delete_us:\n",
+ "for x in tqdm(delete_us, total=len(delete_us)):\n",
" try:\n",
" x.delete()\n",
" except:\n",
@@ -130,6 +100,16 @@
"id": "85c6f39d",
"metadata": {},
"outputs": [],
+ "source": [
+ "delete_us"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab5120e2",
+ "metadata": {},
+ "outputs": [],
"source": []
}
],
From dec69bbfba9f9da1c22892657b1f3512a5cc5138 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Fri, 16 Feb 2024 10:26:26 +0100
Subject: [PATCH 12/19] load latest on file on startup
---
download_files.sh | 3 ++-
start-server.sh | 3 +++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/download_files.sh b/download_files.sh
index 1cfa897..9ce2722 100755
--- a/download_files.sh
+++ b/download_files.sh
@@ -3,4 +3,5 @@ wget -O /opt/app/media/listevent.xml https://oeawcloud.oeaw.ac.at/index.php/s/DL
wget -O /opt/app/media/listorg.xml https://oeawcloud.oeaw.ac.at/index.php/s/a5qCWqzCRnbRkMt/download/listorg.xml
wget -O /opt/app/media/listbibl.xml https://oeawcloud.oeaw.ac.at/index.php/s/bwT4yWGdBeLkSP3/download/listbibl.xml
wget -O /opt/app/media/listplace.xml https://oeawcloud.oeaw.ac.at/index.php/s/Kbnien5KfnPaFsK/download/listplace.xml
-wget -O /opt/app/media/listperson.xml https://oeawcloud.oeaw.ac.at/index.php/s/sRyi2H54eaCiCXJ/download/listperson.xml
\ No newline at end of file
+wget -O /opt/app/media/listperson.xml https://oeawcloud.oeaw.ac.at/index.php/s/sRyi2H54eaCiCXJ/download/listperson.xml
+wget -O /opt/app/media/relations.csv https://oeawcloud.oeaw.ac.at/index.php/s/2BWWMY8etP6tEj7/download/relations.csv
\ No newline at end of file
diff --git a/start-server.sh b/start-server.sh
index ab8d596..3e3762e 100755
--- a/start-server.sh
+++ b/start-server.sh
@@ -6,4 +6,7 @@ touch /var/log/cron.log
cron &&\
python manage.py migrate
python manage.py collectstatic --no-input
+./download_files.sh
+python manage.py find_duplicated_persons
+python manage.py find_duplicated_places
gunicorn pmb.wsgi --user www-data --bind 0.0.0.0:8010 --workers 3 --timeout 600 & nginx -g "daemon off;"
\ No newline at end of file
From f145ea4c6724150c9d0bcc53a1d42700d0f2f7eb Mon Sep 17 00:00:00 2001
From: csae8092
Date: Sun, 18 Feb 2024 16:48:37 +0100
Subject: [PATCH 13/19] merging of entities should keep references and labels
of A and B
---
.../commands/find_duplicated_places.py | 19 ++++++++---------
apis_core/apis_entities/tests.py | 21 +++++++++++++++++++
apis_core/apis_metainfo/models.py | 18 +++++++++++++++-
.../management/commands/dump_relations.py | 4 +++-
4 files changed, 50 insertions(+), 12 deletions(-)
diff --git a/apis_core/apis_entities/management/commands/find_duplicated_places.py b/apis_core/apis_entities/management/commands/find_duplicated_places.py
index 0742aea..17fff19 100644
--- a/apis_core/apis_entities/management/commands/find_duplicated_places.py
+++ b/apis_core/apis_entities/management/commands/find_duplicated_places.py
@@ -16,16 +16,15 @@ class Command(BaseCommand):
def handle(self, *args: Any, **options: Any) -> str | None:
print("searching for potential duplicates")
- props = [
- "id",
- "name",
- "lat",
- "lng"
- ]
- df = pd.DataFrame(
- Place.objects.values_list(*props),
- columns=props,
- ).astype("str").fillna("nix")
+ props = ["id", "name", "lat", "lng"]
+ df = (
+ pd.DataFrame(
+ Place.objects.values_list(*props),
+ columns=props,
+ )
+ .astype("str")
+ .fillna("nix")
+ )
df["custom_index"] = df["id"].astype(str) + " " + df["name"]
df.set_index("custom_index", inplace=True)
indexer = recordlinkage.Index()
diff --git a/apis_core/apis_entities/tests.py b/apis_core/apis_entities/tests.py
index 78928b2..fe795bb 100644
--- a/apis_core/apis_entities/tests.py
+++ b/apis_core/apis_entities/tests.py
@@ -138,6 +138,27 @@ def test_009_merge_view(self):
after = Person.objects.all().count()
self.assertTrue(before > after)
+ def test_009a_merge_notesandreferences(self):
+ source_one = Person.objects.create(
+ name="Person which will be merged",
+ notes="notes_one",
+ references="references_one",
+ )
+ source_two = Person.objects.create(
+ name="Person two which will be merged",
+ )
+ target = Person.objects.create(
+ name="Person which will be kept",
+ notes="target_notes",
+ references="target_references",
+ )
+ target.merge_with(source_one.id)
+ self.assertTrue("notes_one" in target.notes)
+ self.assertTrue("target_notes" in target.notes)
+ self.assertTrue("references_one" in target.references)
+ self.assertTrue("target_references" in target.references)
+ target.merge_with(source_two)
+
def test_010_delete_views(self):
client.login(**USER)
for x in MODELS:
diff --git a/apis_core/apis_metainfo/models.py b/apis_core/apis_metainfo/models.py
index 4a7945b..2a1df6d 100644
--- a/apis_core/apis_metainfo/models.py
+++ b/apis_core/apis_metainfo/models.py
@@ -288,7 +288,13 @@ def merge_with(self, entities):
rels = ContentType.objects.filter(
app_label="apis_relations", model__icontains=e_a
)
+ notes = []
+ references = []
for ent in entities:
+ if isinstance(ent.notes, str):
+ notes.append(ent.notes)
+ if isinstance(ent.references, str):
+ references.append(ent.references)
e_b = type(ent).__name__
e_b_pk = ent.pk
if e_b_pk == e_a_pk:
@@ -332,8 +338,18 @@ def merge_with(self, entities):
for t in k:
setattr(t, "related_{}".format(e_a.lower()), self)
t.save()
-
ent.delete()
+ save_target = False
+ if len(notes) > 0:
+ additional_notes = " ".join(notes)
+ self.notes = f"{self.notes} {additional_notes}"
+ save_target = True
+ if len(references) > 0:
+ additional_references = " ".join(references)
+ self.references = f"{self.references} {additional_references}"
+ save_target = True
+ if save_target:
+ self.save()
class Source(models.Model):
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
index 52979f8..746c0b0 100644
--- a/apis_core/apis_relations/management/commands/dump_relations.py
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -41,7 +41,9 @@ def handle(self, *args: Any, **options: Any) -> str | None:
df = pd.read_csv(save_path).fillna("nodate")
df.set_index("relation_pk", inplace=True, drop=False)
indexer = recordlinkage.Index()
- indexer.block(["relation_type", "source_id", "target_id", "start_date", "end_date"])
+ indexer.block(
+ ["relation_type", "source_id", "target_id", "start_date", "end_date"]
+ )
duplicates = indexer.index(df)
print(f"deleting {len(duplicates)} duplicated relations")
From d989215f99730331fab542d606ce57b1d2d78cab Mon Sep 17 00:00:00 2001
From: csae8092
Date: Sun, 18 Feb 2024 22:29:49 +0100
Subject: [PATCH 14/19] added mgm cmd for merging duplicates 147
---
.../commands/merge_duplicated_entities.py | 63 +++++++++++++++++++
1 file changed, 63 insertions(+)
create mode 100644 apis_core/apis_entities/management/commands/merge_duplicated_entities.py
diff --git a/apis_core/apis_entities/management/commands/merge_duplicated_entities.py b/apis_core/apis_entities/management/commands/merge_duplicated_entities.py
new file mode 100644
index 0000000..c9a8a48
--- /dev/null
+++ b/apis_core/apis_entities/management/commands/merge_duplicated_entities.py
@@ -0,0 +1,63 @@
+import pandas as pd
+
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.management.base import BaseCommand
+from tqdm import tqdm
+
+from apis_core.apis_metainfo.models import TempEntityClass
+
+
+def get_id(row, col):
+ return int(row[col].split(" ")[0])
+
+
+def is_greater(row, col_a, col_b):
+ if row[col_a] > row[col_b]:
+ return True
+ else:
+ return False
+
+
+class Command(BaseCommand):
+ help = """merges duplicated entities\
+ e.g. python manage.py merge_duplicated_entities --csv https://pmb.acdh.oeaw.ac.at/media/duplicated_places.csv
+ """
+
+ def add_arguments(self, parser):
+ parser.add_argument("--csv")
+
+ def handle(self, *args, **kwargs):
+ csv_url = kwargs["csv"]
+ print(f"reading duplicated objects from csv: {csv_url}")
+ df = pd.read_csv(csv_url)
+ df["id_a"] = df.apply(lambda row: get_id(row, "custom_index_1"), axis=1)
+ df["id_b"] = df.apply(lambda row: get_id(row, "custom_index_2"), axis=1)
+ df = df[["id_a", "id_b"]]
+ df["b_smaller_a"] = df.apply(
+ lambda row: is_greater(row, "id_a", "id_b"), axis=1
+ )
+
+ keep_not_found = set()
+ merge_did_not_work = []
+ print(f"start merging of {len(df)} duplicated objects")
+ for i, row in tqdm(df.iterrows(), total=len(df)):
+ if row["b_smaller_a"]:
+ try:
+ keep = TempEntityClass.objects.get(
+ id=row["id_b"]
+ ).get_child_entity()
+ except ObjectDoesNotExist:
+ keep_not_found.add(row["id_b"])
+ try:
+ keep.merge_with(row["id_a"])
+ except Exception as e:
+ merge_did_not_work.append([row, e])
+ if len(keep_not_found) > 0:
+ print("following potential to keep objects could not be found")
+ for x in keep_not_found:
+ print(x)
+ if len(merge_did_not_work) > 0:
+ print("for following objects the merge did not work")
+ for x in merge_did_not_work:
+ print(x)
+ print("done")
From bda94c2de8ad9cb10aabc8572f4dc926f74ec74d Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 19 Feb 2024 10:47:44 +0100
Subject: [PATCH 15/19] remove herman bahr textverzeichnis references #17 [skip
ci]
---
issue__17_remove_references.ipynb | 74 +++++++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
create mode 100644 issue__17_remove_references.ipynb
diff --git a/issue__17_remove_references.ipynb b/issue__17_remove_references.ipynb
new file mode 100644
index 0000000..0ceef30
--- /dev/null
+++ b/issue__17_remove_references.ipynb
@@ -0,0 +1,74 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fa8492cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "from tqdm import tqdm\n",
+ "from django.core.exceptions import ObjectDoesNotExist"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e92f4f2d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for x in tqdm(TempEntityClass.objects.filter(references__icontains=\"Hermann Bahr: Tagebücher\")):\n",
+ " keep = x.references.split(\"Hermann Bahr: Tagebücher\")[0]\n",
+ " if keep == \"\\n\":\n",
+ " x.references = \"\"\n",
+ " elif \"None\" in keep:\n",
+ " x.references = \"\"\n",
+ " else:\n",
+ " x.references = keep\n",
+ " x.save()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f9306d9d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for x in TempEntityClass.objects.filter(references__icontains=\"Hermann Bahr: Tagebücher\"):\n",
+ " print(x.references)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b7ae09b4",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Django Shell-Plus",
+ "language": "python",
+ "name": "django_extensions"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
From ab003e1666b3f9512e29579f8522ddb512593bf2 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 19 Feb 2024 10:56:43 +0100
Subject: [PATCH 16/19] minor [skip ci] #17
---
issue__17_remove_references.ipynb | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/issue__17_remove_references.ipynb b/issue__17_remove_references.ipynb
index 0ceef30..610b14c 100644
--- a/issue__17_remove_references.ipynb
+++ b/issue__17_remove_references.ipynb
@@ -1,5 +1,13 @@
{
"cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b79973f4",
+ "metadata": {},
+ "source": [
+ "## run 2024-02-19"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -40,14 +48,6 @@
"for x in TempEntityClass.objects.filter(references__icontains=\"Hermann Bahr: Tagebücher\"):\n",
" print(x.references)"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "b7ae09b4",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
From d0775a53b53f4fc831677b63c13ccf1f337ac77c Mon Sep 17 00:00:00 2001
From: csae8092
Date: Sun, 25 Feb 2024 12:45:09 +0100
Subject: [PATCH 17/19] wip network serialisation
---
.gitignore | 2 +
apis_core/apis_entities/models.py | 10 ++++
.../management/commands/dump_relations.py | 57 +++++++++++++++++--
apis_core/apis_relations/models.py | 4 +-
apis_core/utils.py | 1 -
pmb/settings.py | 9 +++
requirements.txt | 3 +-
7 files changed, 79 insertions(+), 7 deletions(-)
diff --git a/.gitignore b/.gitignore
index c0aaae0..8062951 100644
--- a/.gitignore
+++ b/.gitignore
@@ -181,3 +181,5 @@ media/duplicated_*.csv
Untitled.ipynb
listevent.xml
relations.csv
+hansi.*
+media/relations.gexf
diff --git a/apis_core/apis_entities/models.py b/apis_core/apis_entities/models.py
index ca4d672..9efe364 100644
--- a/apis_core/apis_entities/models.py
+++ b/apis_core/apis_entities/models.py
@@ -512,6 +512,8 @@ def save(self, *args, **kwargs):
return self
class Meta:
+ verbose_name = "Person"
+ verbose_name_plural = "Personen"
ordering = [
"id",
]
@@ -541,6 +543,8 @@ def save(self, *args, **kwargs):
return self
class Meta:
+ verbose_name = "Ort"
+ verbose_name_plural = "Orte"
ordering = [
"id",
]
@@ -562,6 +566,8 @@ class Institution(AbstractEntity):
)
class Meta:
+ verbose_name = "Institution"
+ verbose_name_plural = "Institutionen"
ordering = [
"id",
]
@@ -583,6 +589,8 @@ class Event(AbstractEntity):
)
class Meta:
+ verbose_name = "Ereignis"
+ verbose_name_plural = "Ereignisse"
ordering = [
"id",
]
@@ -602,6 +610,8 @@ class Work(AbstractEntity):
kind = models.ForeignKey(WorkType, blank=True, null=True, on_delete=models.SET_NULL)
class Meta:
+ verbose_name = "Werk"
+ verbose_name_plural = "Werke"
ordering = [
"id",
]
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
index 746c0b0..5eb777a 100644
--- a/apis_core/apis_relations/management/commands/dump_relations.py
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -1,13 +1,14 @@
import os
import pandas as pd
+import networkx as nx
import recordlinkage
+from tqdm import tqdm
from datetime import datetime
from django.conf import settings
from django.core.management.base import BaseCommand
from icecream import ic
-from tqdm import tqdm
from typing import Any
from apis_core.apis_relations.models import AbstractRelation
from dumper.utils import upload_files_to_owncloud, write_report
@@ -61,14 +62,62 @@ def handle(self, *args: Any, **options: Any) -> str | None:
df.drop(deleted)
save_path = os.path.join(settings.MEDIA_ROOT, "relations.csv")
df.to_csv(save_path, index=False)
- end_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
- report = [os.path.basename(__file__), start_time, end_time]
- write_report(report)
print(f"serialized {len(df)} relations")
files = list()
files.append(save_path)
try:
upload_files_to_owncloud(files)
+ print(f"uploading {save_path} to owncloud")
except Exception as e:
ic(e)
+ print("and now serialize relations as network graph")
+ colors = settings.PMB_COLORS
+ G = nx.Graph()
+ for i, row in tqdm(df.iterrows(), total=len(df)):
+ G.add_nodes_from(
+ [
+ (
+ row["source_id"],
+ {
+ "label": row["source"],
+ "type": row["source_type"],
+ "color": colors[row["source_type"]],
+ },
+ )
+ ]
+ )
+ G.add_nodes_from(
+ [
+ (
+ row["target_id"],
+ {
+ "label": row["target"],
+ "type": row["target_type"],
+ "color": colors[row["target_type"]],
+ },
+ )
+ ]
+ )
+ G.add_edges_from(
+ [
+ (
+ row["source_id"],
+ row["target_id"],
+ {"label": row["relation_type"], "id": row["relation_pk"]},
+ )
+ ]
+ )
+ save_path = os.path.join(settings.MEDIA_ROOT, "relations.gexf")
+ nx.write_gexf(G, save_path)
+ print(f"serialized {len(df)} relations")
+ files = list()
+ files.append(save_path)
+ try:
+ upload_files_to_owncloud(files)
+ print(f"uploading {save_path} to owncloud")
+ except Exception as e:
+ ic(e)
+ end_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
+ report = [os.path.basename(__file__), start_time, end_time]
+ write_report(report)
return "done"
diff --git a/apis_core/apis_relations/models.py b/apis_core/apis_relations/models.py
index 0c2e12e..06cd335 100644
--- a/apis_core/apis_relations/models.py
+++ b/apis_core/apis_relations/models.py
@@ -53,9 +53,11 @@ def get_web_object(self):
"relation_pk": self.pk,
"relation_type": self.relation_type.name,
"source": namea,
- "target": nameb,
"source_id": namea.id,
+ "source_type": namea._meta.verbose_name,
+ "target": nameb,
"target_id": nameb.id,
+ "target_type": nameb._meta.verbose_name,
"start_date": self.start_date_written,
"end_date": self.end_date_written,
}
diff --git a/apis_core/utils.py b/apis_core/utils.py
index 2f64f43..e392733 100644
--- a/apis_core/utils.py
+++ b/apis_core/utils.py
@@ -3,7 +3,6 @@
from django.shortcuts import get_object_or_404
-# from apis_core.apis_entities.detail_views import get_object_from_pk_or_uri
from apis_core.apis_metainfo.models import TempEntityClass, Uri
diff --git a/pmb/settings.py b/pmb/settings.py
index 0ce631e..6b2dd75 100644
--- a/pmb/settings.py
+++ b/pmb/settings.py
@@ -298,3 +298,12 @@
],
},
}
+
+PMB_COLORS = {
+ "Person": "#720e07",
+ "Ort": "#5bc0eb",
+ "Werk": "#ff8600",
+ "Ereignis": "#9bc53d",
+ "Institution": "#1d3461",
+ "PMB": "#9B5F98",
+}
diff --git a/requirements.txt b/requirements.txt
index 95c4b56..86b9d69 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ acdh-id-reconciler>=0.2,<1
acdh-tei-pyutils>=1.1,<2
acdh-wikidata-pyutils==1.0
apis-override-select2js==0.1
-Django>4.1,<6
+Django>=5.0,<6
django-admin-csvexport
django-autocomplete-light
django-crispy-forms
@@ -16,6 +16,7 @@ pandas
pylobid
psycopg2
pyocclient==0.6
+networkx>=3.2.1,<4
icecream
flake8
black
From 34a0cc5c97311735cffc976726d29c578949460c Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 26 Feb 2024 15:42:29 +0100
Subject: [PATCH 18/19] wip network #157
---
.gitignore | 2 ++
apis_core/apis_entities/models.py | 20 +++++++++++++++++++
.../management/commands/dump_relations.py | 13 ++++++++----
apis_core/apis_relations/models.py | 18 +++++++++++++----
pmb/settings.py | 9 ---------
5 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/.gitignore b/.gitignore
index 8062951..e0235b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -183,3 +183,5 @@ listevent.xml
relations.csv
hansi.*
media/relations.gexf
+edges.csv
+nodes.csv
diff --git a/apis_core/apis_entities/models.py b/apis_core/apis_entities/models.py
index 9efe364..6e4870d 100644
--- a/apis_core/apis_entities/models.py
+++ b/apis_core/apis_entities/models.py
@@ -528,6 +528,10 @@ def get_api_url(self):
def get_icon(self):
return "bi bi-people apis-person"
+ @classmethod
+ def get_color(self):
+ return "#720e07"
+
class Place(AbstractEntity):
kind = models.ForeignKey(
@@ -559,6 +563,10 @@ def get_api_url(self):
def get_icon(self):
return "bi bi-map apis-place"
+ @classmethod
+ def get_color(self):
+ return "#5bc0eb"
+
class Institution(AbstractEntity):
kind = models.ForeignKey(
@@ -582,6 +590,10 @@ def get_api_url(self):
def get_icon(self):
return "bi bi-building-gear apis-institution"
+ @classmethod
+ def get_color(self):
+ return "#1d3461"
+
class Event(AbstractEntity):
kind = models.ForeignKey(
@@ -605,6 +617,10 @@ def get_api_url(self):
def get_icon(self):
return "bi bi-calendar3 apis-event"
+ @classmethod
+ def get_color(self):
+ return "#9bc53d"
+
class Work(AbstractEntity):
kind = models.ForeignKey(WorkType, blank=True, null=True, on_delete=models.SET_NULL)
@@ -626,6 +642,10 @@ def get_api_url(self):
def get_icon(self):
return "bi bi-book apis-work"
+ @classmethod
+ def get_color(self):
+ return "#ff8600"
+
a_ents = getattr(settings, "APIS_ADDITIONAL_ENTITIES", False)
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
index 5eb777a..14d79dc 100644
--- a/apis_core/apis_relations/management/commands/dump_relations.py
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -43,7 +43,13 @@ def handle(self, *args: Any, **options: Any) -> str | None:
df.set_index("relation_pk", inplace=True, drop=False)
indexer = recordlinkage.Index()
indexer.block(
- ["relation_type", "source_id", "target_id", "start_date", "end_date"]
+ [
+ "relation_type",
+ "source_id",
+ "target_id",
+ "relation_start_date_written",
+ "relation_end_date_written",
+ ]
)
duplicates = indexer.index(df)
print(f"deleting {len(duplicates)} duplicated relations")
@@ -71,7 +77,6 @@ def handle(self, *args: Any, **options: Any) -> str | None:
except Exception as e:
ic(e)
print("and now serialize relations as network graph")
- colors = settings.PMB_COLORS
G = nx.Graph()
for i, row in tqdm(df.iterrows(), total=len(df)):
G.add_nodes_from(
@@ -81,7 +86,7 @@ def handle(self, *args: Any, **options: Any) -> str | None:
{
"label": row["source"],
"type": row["source_type"],
- "color": colors[row["source_type"]],
+ "color": row["source_color"],
},
)
]
@@ -93,7 +98,7 @@ def handle(self, *args: Any, **options: Any) -> str | None:
{
"label": row["target"],
"type": row["target_type"],
- "color": colors[row["target_type"]],
+ "color": row["target_color"],
},
)
]
diff --git a/apis_core/apis_relations/models.py b/apis_core/apis_relations/models.py
index 06cd335..1578633 100644
--- a/apis_core/apis_relations/models.py
+++ b/apis_core/apis_relations/models.py
@@ -52,14 +52,24 @@ def get_web_object(self):
result = {
"relation_pk": self.pk,
"relation_type": self.relation_type.name,
- "source": namea,
+ "relation_class": f"{namea._meta.verbose_name} -> {nameb._meta.verbose_name}",
+ "relation_name": self.__str__(),
+ "relation_start_date": f"{self.start_date}",
+ "relation_end_date": f"{self.end_date}",
+ "relation_start_date_written": f"{self.start_date_written}",
+ "relation_end_date_written": f"{self.end_date_written}",
+ "source": namea.__str__(),
"source_id": namea.id,
"source_type": namea._meta.verbose_name,
- "target": nameb,
+ "source_start_date": f"{namea.start_date}",
+ "source_start_date_written": f"{namea.start_date_written}",
+ "source_color": namea.get_color(),
+ "target": nameb.__str__(),
"target_id": nameb.id,
"target_type": nameb._meta.verbose_name,
- "start_date": self.start_date_written,
- "end_date": self.end_date_written,
+ "target_start_date": f"{nameb.start_date}",
+ "target_start_date_written": f"{nameb.start_date_written}",
+ "target_color": nameb.get_color(),
}
return result
diff --git a/pmb/settings.py b/pmb/settings.py
index 6b2dd75..0ce631e 100644
--- a/pmb/settings.py
+++ b/pmb/settings.py
@@ -298,12 +298,3 @@
],
},
}
-
-PMB_COLORS = {
- "Person": "#720e07",
- "Ort": "#5bc0eb",
- "Werk": "#ff8600",
- "Ereignis": "#9bc53d",
- "Institution": "#1d3461",
- "PMB": "#9B5F98",
-}
From 5a90b6875e87863b9059061a5e67768cba402155 Mon Sep 17 00:00:00 2001
From: csae8092
Date: Mon, 26 Feb 2024 17:00:08 +0100
Subject: [PATCH 19/19] almost done?
---
.../management/commands/dump_relations.py | 97 ++++++++++++-------
1 file changed, 63 insertions(+), 34 deletions(-)
diff --git a/apis_core/apis_relations/management/commands/dump_relations.py b/apis_core/apis_relations/management/commands/dump_relations.py
index 14d79dc..7d468ea 100644
--- a/apis_core/apis_relations/management/commands/dump_relations.py
+++ b/apis_core/apis_relations/management/commands/dump_relations.py
@@ -66,62 +66,91 @@ def handle(self, *args: Any, **options: Any) -> str | None:
break
print(deleted)
df.drop(deleted)
- save_path = os.path.join(settings.MEDIA_ROOT, "relations.csv")
- df.to_csv(save_path, index=False)
+ relations_csv = os.path.join(settings.MEDIA_ROOT, "relations.csv")
+ df.to_csv(relations_csv, index=False)
print(f"serialized {len(df)} relations")
files = list()
- files.append(save_path)
- try:
- upload_files_to_owncloud(files)
- print(f"uploading {save_path} to owncloud")
- except Exception as e:
- ic(e)
+ files.append(relations_csv)
+
print("and now serialize relations as network graph")
G = nx.Graph()
+ nodes = {}
+ edges = []
+ edges_labels = ["source", "target", "type", "label", "date"]
for i, row in tqdm(df.iterrows(), total=len(df)):
- G.add_nodes_from(
+ source_node = {
+ "id": row["source_id"],
+ "label": row["source"],
+ "type": row["source_type"],
+ "color": row["source_color"],
+ "start_date": row["source_start_date"],
+ "start_date_written": row["source_start_date_written"],
+ }
+ nodes[row["source_id"]] = source_node
+ G.add_nodes_from([(row["source_id"], source_node)])
+ target_node = {
+ "id": row["target_id"],
+ "label": row["target"],
+ "type": row["target_type"],
+ "color": row["target_color"],
+ "date": row["target_start_date"],
+ "start_date_written": row["target_start_date_written"],
+ }
+ nodes[row["target_id"]] = target_node
+ G.add_nodes_from([(row["target_id"], target_node)])
+ G.add_edges_from(
[
(
row["source_id"],
- {
- "label": row["source"],
- "type": row["source_type"],
- "color": row["source_color"],
- },
- )
- ]
- )
- G.add_nodes_from(
- [
- (
row["target_id"],
{
- "label": row["target"],
- "type": row["target_type"],
- "color": row["target_color"],
+ "relation_class": row["relation_class"],
+ "label": row["relation_type"],
+ "id": row["relation_pk"],
+ "start_date": row["relation_start_date"],
+ "start_date_written": row["relation_start_date_written"],
+ "end_date": row["relation_end_date"],
+ "end_date_written": row["relation_end_date_written"],
},
)
]
)
- G.add_edges_from(
+ edges.append(
[
- (
- row["source_id"],
- row["target_id"],
- {"label": row["relation_type"], "id": row["relation_pk"]},
- )
+ row["source_id"],
+ row["target_id"],
+ row["relation_class"],
+ row["relation_type"],
+ row["relation_start_date"],
]
)
- save_path = os.path.join(settings.MEDIA_ROOT, "relations.gexf")
- nx.write_gexf(G, save_path)
+ gexf_file = os.path.join(settings.MEDIA_ROOT, "relations.gexf")
+ nx.write_gexf(G, gexf_file)
print(f"serialized {len(df)} relations")
- files = list()
- files.append(save_path)
+ files.append(gexf_file)
+
+ ndf = pd.DataFrame(edges, columns=edges_labels)
+ edges_file = os.path.join(settings.MEDIA_ROOT, "edges.csv")
+ ndf.to_csv(edges_file, index=False)
+ files.append(edges_file)
+
+ data = []
+ for key, value in nodes.items():
+ data.append(value)
+
+ df = pd.DataFrame(data)
+ nodes_file = os.path.join(settings.MEDIA_ROOT, "nodes.csv")
+ df.to_csv(nodes_file, index=False)
+ files.append(nodes_file)
+ ic(files)
+
try:
upload_files_to_owncloud(files)
- print(f"uploading {save_path} to owncloud")
+ for x in files:
+ print(f"uploading {x} to owncloud")
except Exception as e:
ic(e)
+
end_time = datetime.now().strftime(settings.PMB_TIME_PATTERN)
report = [os.path.basename(__file__), start_time, end_time]
write_report(report)