summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2026-03-10 17:26:54 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2026-03-25 15:10:32 +0100
commit14f2a0a368a00a217b5657c0ed903be897d58294 (patch)
treeb9e1e1aa11afc6687e73854e175441b0f3b3e3e2
parente79327d84c8e18b83e8528b707d064a89259b1eb (diff)
downloadIshtar-14f2a0a368a00a217b5657c0ed903be897d58294.tar.bz2
Ishtar-14f2a0a368a00a217b5657c0ed903be897d58294.zip
✨ qualified biographical note - models
-rw-r--r--ishtar_common/admin.py15
-rw-r--r--ishtar_common/migrations/0274_qualifiedbiographicalnote.py78
-rw-r--r--ishtar_common/models.py80
3 files changed, 173 insertions, 0 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index b8cae9fa8..8f9d4d0a9 100644
--- a/ishtar_common/admin.py
+++ b/ishtar_common/admin.py
@@ -1053,6 +1053,20 @@ class BiographicalNoteAdmin(admin.ModelAdmin):
)
+@admin.register(models.QualifiedBiographicalNote, site=admin_site)
+class QualifiedBiographicalNoteAdmin(admin.ModelAdmin):
+ list_display = ("biographical_note", "qualification_type")
+ autocomplete_fields = ["biographical_note"]
+ model = models.QualifiedBiographicalNote
+ exclude = ("search_vector", "uuid", "cached_label")
+ search_fields = (
+ "biographical_note__denomination",
+ "biographical_note__last_name",
+ "biographical_note__first_name",
+ "biographical_note__slug",
+ )
+
+
@admin.register(models.GDPRLog, site=admin_site)
class GDPRLogAdmin(admin.ModelAdmin):
list_display = ("user", "date", "ip", "routable_ip", "activity")
@@ -1685,6 +1699,7 @@ general_models = [
models.LicenseType,
models.PersonType,
models.ShootingAngle,
+ models.QualifiedBiographicalNoteType,
models_common.GeoProviderType,
models_common.GeoDataType,
models_common.GeoOriginType,
diff --git a/ishtar_common/migrations/0274_qualifiedbiographicalnote.py b/ishtar_common/migrations/0274_qualifiedbiographicalnote.py
new file mode 100644
index 000000000..4713937cc
--- /dev/null
+++ b/ishtar_common/migrations/0274_qualifiedbiographicalnote.py
@@ -0,0 +1,78 @@
+# Generated by Django 4.2.19 on 2026-03-24 15:39
+
+from django.conf import settings
+import django.contrib.postgres.search
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import ishtar_common.models
+import ishtar_common.models_common
+import re
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('ishtar_common', '0273_searchcache'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='documenttemplate',
+ name='export_format',
+ field=models.CharField(blank=True, choices=[('docx', 'DOCX'), ('html', 'HTML'), ('pdf', 'PDF'), ('xlsx', 'XLSX')], default='', max_length=4, verbose_name='Export format'),
+ ),
+ migrations.CreateModel(
+ name='QualifiedBiographicalNoteType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('label', models.TextField(verbose_name='Label')),
+ ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID')),
+ ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
+ ('available', models.BooleanField(default=True, verbose_name='Available')),
+ ('order', models.IntegerField(default=1, verbose_name='Order')),
+ ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.qualifiedbiographicalnotetype', verbose_name='Parent')),
+ ],
+ options={
+ 'verbose_name': 'Qualification type',
+ 'verbose_name_plural': 'Qualification types',
+ 'ordering': ['order', 'label'],
+ },
+ bases=(ishtar_common.models_common.Cached, models.Model),
+ ),
+ migrations.CreateModel(
+ name='QualifiedBiographicalNote',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('search_vector', django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector')),
+ ('timestamp_geo', models.IntegerField(blank=True, null=True, verbose_name='Timestamp geo')),
+ ('timestamp_label', models.IntegerField(blank=True, null=True, verbose_name='Timestamp label')),
+ ('data', models.JSONField(blank=True, default=dict)),
+ ('last_modified', models.DateTimeField(blank=True, default=django.utils.timezone.now)),
+ ('created', models.DateTimeField(blank=True, default=django.utils.timezone.now)),
+ ('history_m2m', models.JSONField(blank=True, default=dict)),
+ ('need_update', models.BooleanField(default=False, verbose_name='Need update')),
+ ('locked', models.BooleanField(default=False, verbose_name='Item locked for edition')),
+ ('uuid', models.UUIDField(default=uuid.uuid4)),
+ ('cached_label', models.TextField(blank=True, db_index=True, default='', verbose_name='Cached name')),
+ ('biographical_note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualified_biographicalnote', to='ishtar_common.biographicalnote', verbose_name='Biographical note')),
+ ('history_creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
+ ('history_modifier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Last editor')),
+ ('imports', models.ManyToManyField(blank=True, related_name='imported_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Created by imports')),
+ ('imports_updated', models.ManyToManyField(blank=True, related_name='import_updated_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Updated by imports')),
+ ('ishtar_users', models.ManyToManyField(blank=True, related_name='%(class)s_associated', to='ishtar_common.ishtaruser')),
+ ('lock_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Locked by')),
+ ('qualification_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.qualifiedbiographicalnotetype', verbose_name='Qualification type')),
+ ],
+ options={
+ 'verbose_name': 'Qualified biographical note',
+ 'verbose_name_plural': 'Qualified biographical notes',
+ 'ordering': ('qualification_type__order', 'biographical_note__denomination'),
+ 'permissions': (('view_own_qualifiedbiographicalnote', 'Can view own Qualified biographical note'), ('change_own_qualifiedbiographicalnote', 'Can change own Qualified biographical note'), ('delete_own_qualifiedbiographicalnote', 'Can delete own Qualified biographical note')),
+ },
+ bases=(ishtar_common.models_common.StatisticItem, ishtar_common.models_common.TemplateItem, models.Model, ishtar_common.models_common.CachedGen, ishtar_common.models_common.FixAssociated, ishtar_common.models.ValueGetter, ishtar_common.models_common.MainItem),
+ ),
+ ]
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 65dddc051..90e8a8d8d 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -4667,6 +4667,86 @@ def author_post_save(sender, **kwargs):
post_save.connect(author_post_save, sender=Author)
+class QualifiedBiographicalNoteType(OrderedHierarchicalType):
+ order = models.IntegerField(_("Order"), default=10)
+
+ class Meta:
+ verbose_name = _("Qualification type")
+ verbose_name_plural = _("Qualification types")
+ ordering = ["order", "label"]
+ ADMIN_SECTION = _("Directory")
+
+
+post_save.connect(post_save_cache, sender=QualifiedBiographicalNoteType)
+post_delete.connect(post_save_cache, sender=QualifiedBiographicalNoteType)
+
+
+class QualifiedBiographicalNote(BaseHistorizedItem, ValueGetter, MainItem):
+ SLUG = "qualifiedbiographicalnote"
+ PARENT_SEARCH_VECTORS = ["biographical_note"]
+
+ uuid = models.UUIDField(default=uuid.uuid4)
+ biographical_note = models.ForeignKey(
+ BiographicalNote,
+ verbose_name=_("Biographical note"),
+ related_name="qualified_biographicalnote",
+ on_delete=models.CASCADE,
+ )
+ qualification_type = models.ForeignKey(
+ QualifiedBiographicalNoteType, verbose_name=_("Qualification type"),
+ on_delete=models.PROTECT
+ )
+ cached_label = models.TextField(
+ _("Cached name"), blank=True, default="", db_index=True
+ )
+ objects = UUIDModelManager()
+
+ class Meta:
+ verbose_name = _("Qualified biographical note")
+ verbose_name_plural = _("Qualified biographical notes")
+ ordering = ("qualification_type__order", "biographical_note__denomination")
+ permissions = (
+ ("view_own_qualifiedbiographicalnote", "Can view own Qualified biographical note"),
+ ("change_own_qualifiedbiographicalnote", "Can change own Qualified biographical note"),
+ ("delete_own_qualifiedbiographicalnote", "Can delete own Qualified biographical note"),
+ )
+ ADMIN_SECTION = _("Directory")
+
+ def __str__(self):
+ return self.cached_label or ""
+
+ def natural_key(self):
+ return (self.uuid,)
+
+ def _generate_cached_label(self):
+ return f"{self.biographical_note} ({self.qualification_type})"
+
+ def public_representation(self):
+ return {"type": str(self.qualification_type),
+ "biographical_note": str(self.biographical_note)}
+
+ def merge(self, item, keep_old=False):
+ merge_model_objects(self, item, keep_old=keep_old)
+
+
+def qualified_post_save(sender, **kwargs):
+ if not kwargs.get("instance"):
+ return
+ cached_label_changed(sender, **kwargs)
+ instance = kwargs.get("instance")
+ q = QualifiedBiographicalNote.objects.filter(
+ biographical_note=instance.biographical_note,
+ qualification_type=instance.qualification_type)
+ if q.count() <= 1:
+ return
+ qualified_persons = list(q.all())
+ for qualified_person in qualified_persons[1:]:
+ qualified_persons[0].merge(qualified_person)
+
+
+post_save.connect(qualified_post_save, sender=QualifiedBiographicalNote)
+
+
class SourceType(OrderedHierarchicalType):
coins_type = models.CharField(
_("COInS export - type"), default="document", max_length=100