diff --git a/apis_ontology/admin.py b/apis_ontology/admin.py new file mode 100644 index 0000000..7fc55ea --- /dev/null +++ b/apis_ontology/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from parler.admin import TranslatableAdmin + +from .models import Person + + +@admin.register(Person) +class PersonAdmin(TranslatableAdmin): + # TODO: allow editing the translations here? + list_display = ["name", "all_languages_column"] + pass diff --git a/apis_ontology/filtersets.py b/apis_ontology/filtersets.py index 7a2d4d0..cbd48e9 100644 --- a/apis_ontology/filtersets.py +++ b/apis_ontology/filtersets.py @@ -1,3 +1,4 @@ +from django.conf.global_settings import LANGUAGE_CODE import django_filters from apis_core.apis_entities.filtersets import ( ABSTRACT_ENTITY_FILTERS_EXCLUDE, @@ -9,6 +10,7 @@ from apis_core.relations.models import Relation from django.apps import apps from django.db import models +from django.utils.translation import get_language from apis_ontology.forms import ( PersonSearchForm, @@ -212,9 +214,10 @@ class Meta: ) def custom_name_search(self, queryset, name, value): - name_query = models.Q(name__icontains=value) | models.Q( - alternative_names__icontains=value - ) + name_query = models.Q( + translations__name__icontains=value, + translations__language_code=get_language(), + ) | models.Q(alternative_names__icontains=value) if value.isdigit(): name_query = name_query | models.Q(pk=int(value)) diff --git a/apis_ontology/forms.py b/apis_ontology/forms.py index 3f538ef..b844311 100644 --- a/apis_ontology/forms.py +++ b/apis_ontology/forms.py @@ -1,9 +1,9 @@ -from apis_core.relations.models import Relation from apis_core.relations.forms import RelationForm from django import forms from apis_core.generic.forms import GenericFilterSetForm, GenericModelForm -from django.forms.models import ModelChoiceField from django.apps import apps +from parler.fields import TranslatedField +from parler.forms import TranslatableModelForm class TibscholEntityForm(GenericModelForm): @@ -35,7 +35,9 @@ class PlaceForm(TibscholEntityForm): ] -class PersonForm(TibscholEntityForm): +class PersonForm(TranslatableModelForm, TibscholEntityForm): + name = TranslatedField() + field_order = [ "name", "alternative_names", @@ -49,6 +51,13 @@ class PersonForm(TibscholEntityForm): "review", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["name"].help_text = ( + "Equivalent transliterations in Tibetan/English inputs will be automatically provided at the time of creation. However, updates to this field will NOT trigger any updates to the values in other languages." + ) + class WorkForm(TibscholEntityForm): field_order = [ diff --git a/apis_ontology/management/commands/support_tibetan_script_001.py b/apis_ontology/management/commands/support_tibetan_script_001.py new file mode 100644 index 0000000..d540ac1 --- /dev/null +++ b/apis_ontology/management/commands/support_tibetan_script_001.py @@ -0,0 +1,26 @@ +import pyewts +from django.core.management.base import BaseCommand +from tqdm.auto import tqdm +from apis_ontology.models import Person +from tqdm.auto import tqdm + +TIBETAN = "es" + + +class Command(BaseCommand): + help = "add Tibetan transliterations for Person - names" + + def handle(self, *args, **options): + converter = pyewts.pyewts() + + for p in tqdm(Person.objects.all()): + tibetan_name = converter.toUnicode(p.name_latin) + p.set_current_language(TIBETAN) + p.name = tibetan_name + p.save() + + self.stdout.write( + self.style.SUCCESS( + f"{len(Person.objects.all())} names were successfully transliterated into Tibetan." + ) + ) diff --git a/apis_ontology/middleware/__init__.py b/apis_ontology/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apis_ontology/migrations/0032_rename_name_person_name_latin_and_more.py b/apis_ontology/migrations/0032_rename_name_person_name_latin_and_more.py new file mode 100644 index 0000000..2b61043 --- /dev/null +++ b/apis_ontology/migrations/0032_rename_name_person_name_latin_and_more.py @@ -0,0 +1,74 @@ +# Generated by Django 5.1.3 on 2024-11-17 00:11 + +import django.db.models.deletion +import parler.fields +import parler.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("apis_ontology", "0031_versioninstancecopiedwrittendownatplace_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="person", + old_name="name", + new_name="name_latin", + ), + migrations.RenameField( + model_name="versionperson", + old_name="name", + new_name="name_latin", + ), + migrations.CreateModel( + name="PersonTranslation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language_code", + models.CharField( + db_index=True, max_length=15, verbose_name="Language" + ), + ), + ( + "name", + models.CharField( + blank=True, default="", max_length=255, verbose_name="Name" + ), + ), + ( + "master", + parler.fields.TranslationsForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="translations", + to="apis_ontology.person", + ), + ), + ], + options={ + "verbose_name": "person Translation", + "db_table": "apis_ontology_person_translation", + "db_tablespace": "", + "managed": True, + "default_permissions": (), + "unique_together": {("language_code", "master")}, + }, + bases=(parler.models.TranslatableModel, models.Model), + ), + migrations.RunSQL( + "INSERT INTO apis_ontology_person_translation(language_code, name , master_id) SELECT 'en', name_latin, rootobject_ptr_id FROM apis_ontology_person;" + ), + ] diff --git a/apis_ontology/models.py b/apis_ontology/models.py index 24a2041..202dd7b 100644 --- a/apis_ontology/models.py +++ b/apis_ontology/models.py @@ -14,6 +14,10 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from parler.models import TranslatableModel, TranslatedFields + +from apis_ontology.querysets import PersonQuerySet +from django.utils.translation import get_language logger = logging.getLogger(__name__) @@ -69,7 +73,12 @@ def uri(self): class Person( - VersionMixin, LegacyStuffMixin, LegacyDateMixin, TibScholEntityMixin, AbstractEntity + VersionMixin, + LegacyStuffMixin, + LegacyDateMixin, + TibScholEntityMixin, + AbstractEntity, + TranslatableModel, ): class_uri = "http://id.loc.gov/ontologies/bibframe/Person" @@ -92,18 +101,27 @@ class Person( ] NATIONALITY = [("Indic", "Indic"), ("Tibetan", "Tibetan")] - name = models.CharField(max_length=255, blank=True, default="", verbose_name="Name") + name_latin = models.CharField( + max_length=255, blank=True, default="", verbose_name="Name" + ) + translations = TranslatedFields( + name=models.CharField( + max_length=255, blank=True, default="", verbose_name="Name" + ) + ) gender = models.CharField(max_length=6, choices=GENDERS, default="male") nationality = models.CharField( max_length=10, choices=NATIONALITY, blank=True, null=True ) + def __str__(self): + return f"{self.name} ({self.pk})" + class Meta: verbose_name = _("person") verbose_name_plural = _("Persons") - def __str__(self): - return f"{self.name} ({self.pk})" + objects = PersonQuerySet.as_manager() class Place( @@ -137,6 +155,8 @@ def __str__(self): class WorkQuerySet(QuerySet): def with_author(self): + current_language = get_language() + return self.annotate( # Subquery to get the Person ID related to the Work through PersonAuthorOfWork author_id=Subquery( @@ -146,7 +166,9 @@ def with_author(self): ), # Subquery to get the Person's name based on the person_id from above author_name=Subquery( - Person.objects.filter(id=OuterRef("author_id")).values("name")[:1] + Person.objects.filter(id=OuterRef("author_id")) + .filter(translations__language_code=current_language) + .values("translations__name")[:1] ), ) @@ -202,6 +224,7 @@ def __str__(self): class InstanceQuerySet(QuerySet): def with_author(self): + current_language = get_language() return self.annotate( # Subquery to get the Person ID related to the Work through PersonAuthorOfWork work_id=Subquery( @@ -216,7 +239,9 @@ def with_author(self): ), # Subquery to get the Person's name based on the person_id from above author_name=Subquery( - Person.objects.filter(id=OuterRef("author_id")).values("name")[:1] + Person.objects.filter(id=OuterRef("author_id")) + .filter(translations__language_code=current_language) + .values("translations__name")[:1] ), ) diff --git a/apis_ontology/querysets.py b/apis_ontology/querysets.py new file mode 100644 index 0000000..2a17199 --- /dev/null +++ b/apis_ontology/querysets.py @@ -0,0 +1,5 @@ +from parler.managers import TranslatableQuerySet + + +class PersonQuerySet(TranslatableQuerySet): + pass diff --git a/apis_ontology/settings/server_settings.py b/apis_ontology/settings/server_settings.py index 147ec53..0fd1d9c 100644 --- a/apis_ontology/settings/server_settings.py +++ b/apis_ontology/settings/server_settings.py @@ -1,4 +1,7 @@ +import os from apis_acdhch_default_settings.settings import * +from django.utils.translation import gettext_lazy as _ +from django.conf.locale import LANG_INFO # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -17,6 +20,7 @@ "apis_core.history", "django_acdhch_functions", "django_select2", + "parler", ] INSTALLED_APPS.remove("apis_ontology") INSTALLED_APPS.insert(0, "apis_ontology") @@ -65,4 +69,51 @@ MIDDLEWARE += [ "simple_history.middleware.HistoryRequestMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", # Enables language switching based on session + # "apis_ontology.middleware.language_change_middleware.LanguageChangeMiddleware", ] + + +LANG_INFO.update( + { + "bo": { + "bidi": False, # Set to True if the language is written right-to-left + "code": "bo", + "name": "Tibetan", + "name_local": "བོད་ཡིག", # Native name + }, + } +) + +LANGUAGE_CODE = "en" # This will be the default language + +# Locale paths (optional if you store translations in a custom directory) +# We currently use only model translations on specific fields +# LOCALE_PATHS = [ +# BASE_DIR / 'locale', +# ] + +PARLER_LANGUAGES = { + None: ( # Site ID 1 + {"code": "en"}, + {"code": "es"}, + ), + "default": { + "fallback": "en", # Use English if translation is missing + "hide_untranslated": False, # Show default values for missing translations + }, +} + + +## List of available languages in the app +LANGUAGES = [ + ("en", _("English")), + ("es", _("Tibetan")), +] + +LOCALE_PATHS = (os.path.join(BASE_DIR, "apis_ontology", "locale/"),) + +USE_I18N = True +USE_L10N = True +USE_TZ = True diff --git a/apis_ontology/signals.py b/apis_ontology/signals.py index f284267..2c174d4 100644 --- a/apis_ontology/signals.py +++ b/apis_ontology/signals.py @@ -1,11 +1,20 @@ import os from django.contrib.auth.signals import user_logged_in +from django.db.models.base import pre_save, post_save from django.dispatch import receiver from django.contrib.auth.models import Group from django.db.models.signals import pre_delete from apis_core.apis_entities.models import RootObject from apis_core.relations.models import Relation +from apis_ontology.models import Person, PersonTranslation +from parler.signals import post_translation_save +from django.dispatch import receiver +import pyewts + +converter = pyewts.pyewts() +TIBETAN = "es" + @receiver(user_logged_in) def add_to_group(sender, user, request, **kwargs): @@ -19,3 +28,58 @@ def add_to_group(sender, user, request, **kwargs): def cascade_delete_related(sender, instance, **kwargs): Relation.objects.filter(subj_object_id=instance.pk).delete() Relation.objects.filter(obj_object_id=instance.pk).delete() + + +@receiver(post_save, sender=Person) +def fill_missing_translations(sender, instance, created, **kwargs): + + if not created: + # Only add translations when a new entity is created + return + + current_language = instance.get_current_language() + target_language = "en" if current_language == TIBETAN else TIBETAN + + # Assumption here is that object is created at first in one language + + def generate_translation(base_instance, base_language, target_language): + # Replace with your translation logic + fields_with_translations = ["name"] + translations = {} + instance.set_current_language(current_language) + + for f in fields_with_translations: + base_lang_val = getattr(base_instance, f) + if target_language == TIBETAN: + translations[f] = { + current_language: base_lang_val, + target_language: converter.toUnicode(base_lang_val), + } + else: + translations[f] = { + current_language: base_lang_val, + target_language: converter.toWylie(base_lang_val), + } + + return translations + + translations = generate_translation(instance, current_language, target_language) + instance.set_current_language(target_language) + for f, tr in translations.items(): + setattr(instance, f, tr[target_language]) + + instance.save() + + +# post save signal to update translations for person.name +@receiver(post_save, sender=Person) +def update_person_translations(sender, instance, **kwargs): + pass + # TODO: Depending on which language is used while creating objects + # we should create the translation object using pyewts + # if instance.name: + # PersonTranslation.objects.update_or_create( + # master=instance, + # language_code="bo", + # defaults={"name": instance.name}, + # ) diff --git a/apis_ontology/tables.py b/apis_ontology/tables.py index e949576..ba3490b 100644 --- a/apis_ontology/tables.py +++ b/apis_ontology/tables.py @@ -6,7 +6,7 @@ from django.template.loader import render_to_string from django.utils.safestring import mark_safe import re - +from apis_core.history.tables import HistoryGenericTable from .models import Instance, Person, Place, Work from .templatetags.filter_utils import ( render_coordinate, @@ -347,3 +347,9 @@ def __init__(self, *args, **kwargs): # + " - " # + (author.end_date_written if author.end_date_written else "") # ) + + +class PersonHistoryTable(tables.Table): + # There is a problem in accessing Name + # TODO: FInd out how to modiy this to use HistoryGenericTable + fields_changed = None diff --git a/apis_ontology/templates/base.html b/apis_ontology/templates/base.html index 1194af0..eff603f 100644 --- a/apis_ontology/templates/base.html +++ b/apis_ontology/templates/base.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load i18n %} {% load static %} {% load tibschol_entity_ctypes %} @@ -32,8 +33,31 @@ Data model -{{ block.super }} -{% endblock main-menu %} + + {{ block.super }} +
+ + {% endblock main-menu %} {% block modal %} {{ block.super }} diff --git a/apis_ontology/urls.py b/apis_ontology/urls.py index 2586a04..411f42d 100644 --- a/apis_ontology/urls.py +++ b/apis_ontology/urls.py @@ -3,15 +3,20 @@ from django.urls import include, path from django.views.generic import TemplateView from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.conf.urls.i18n import i18n_patterns from apis_core.apis_entities.api_views import GetEntityGeneric - +from django.conf.urls.i18n import set_language urlpatterns = [ path("admin/", admin.site.urls), - path("apis/", include("apis_core.urls", namespace="apis")), - path("apis/collections/", include("apis_core.collections.urls")), +] + +# I18N patterns +urlpatterns += i18n_patterns( path("accounts/", include("django.contrib.auth.urls")), + path("apis/", include("apis_core.urls")), + path("apis/collections/", include("apis_core.collections.urls")), path("entity/