Skip to content

Commit a5f6445

Browse files
authored
Feat(admin) add translate actions (#491)
* feat(admin): add actions translate * fix: multiple organizations with same languages * fix(crisalid): related organizations for documents * style: linter
1 parent e26bb5d commit a5f6445

File tree

10 files changed

+104
-23
lines changed

10 files changed

+104
-23
lines changed

apps/accounts/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.utils.safestring import mark_safe
1010
from import_export.admin import ExportActionMixin # type: ignore
1111

12-
from apps.commons.admin import RoleBasedAccessAdmin
12+
from apps.commons.admin import RoleBasedAccessAdmin, TranslateObjectAdminMixin
1313
from apps.emailing.models import Email
1414
from apps.organizations.models import Organization
1515
from apps.projects.models import Project
@@ -20,7 +20,7 @@
2020
from .utils import get_group_permissions
2121

2222

23-
class UserAdmin(ExportActionMixin, RoleBasedAccessAdmin):
23+
class UserAdmin(TranslateObjectAdminMixin, ExportActionMixin, RoleBasedAccessAdmin):
2424
resource_classes = [UserResource]
2525

2626
list_display = (
@@ -156,7 +156,7 @@ def permissions_representations(self, instance: Group) -> str:
156156
return "- " + "\n- ".join(get_group_permissions(instance))
157157

158158

159-
class PeopleGroupAdmin(admin.ModelAdmin):
159+
class PeopleGroupAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
160160
list_display = ("id", "name", "organization", "email")
161161
search_fields = ("name", "email", "id")
162162
filter_horizontal = ("featured_projects",)

apps/commons/admin.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
from django.contrib import admin
1+
from django.contrib import admin, messages
2+
from django.contrib.contenttypes.models import ContentType
23
from django.db.models import QuerySet
34
from guardian.shortcuts import get_objects_for_user
45

56
from apps.accounts.models import ProjectUser
67
from apps.organizations.models import Organization
8+
from services.translator.tasks import translate_object
79

810

911
class RoleBasedAccessAdmin(admin.ModelAdmin):
@@ -101,3 +103,26 @@ def get_context_data(self, **kwargs):
101103
ctx = ctx | self.admin_site.each_context(self.request)
102104
ctx |= self.admin_app or {}
103105
return ctx
106+
107+
108+
class TranslateObjectAdminMixin:
109+
"""Admin Mixin for run translated task on objects"""
110+
111+
def __init__(self, *ar, **kw):
112+
super().__init__(*ar, **kw)
113+
self.actions = getattr(self, "actions", [])
114+
if "translates" not in self.actions:
115+
self.actions = tuple(list(self.actions) + ["translates"])
116+
117+
@admin.action(description="translates manual")
118+
def translates(self, request, queryset):
119+
model = queryset.model
120+
content_type = ContentType.objects.get_for_model(model)
121+
for obj in queryset:
122+
translate_object.apply_async((content_type.id, obj.id))
123+
124+
messages.add_message(
125+
request,
126+
messages.INFO,
127+
f"Translates for {queryset.count()} objects created!",
128+
)

apps/files/admin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from django.contrib import admin
55
from django.db import models
66

7+
from apps.commons.admin import TranslateObjectAdminMixin
8+
79
from .models import Image, ProjectUserAttachmentFile, ProjectUserAttachmentLink
810

911

@@ -65,14 +67,14 @@ class ImageAdmin(admin.ModelAdmin):
6567

6668

6769
@admin.register(ProjectUserAttachmentFile)
68-
class ProjectUserAttachmentFileAdmin(admin.ModelAdmin):
70+
class ProjectUserAttachmentFileAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
6971
list_display = ("id", "owner", "title")
7072
autocomplete_fields = ("owner",)
7173
search_fields = ("owner", "title", "mime")
7274

7375

7476
@admin.register(ProjectUserAttachmentLink)
75-
class ProjectUserAttachmentLinkAdmin(admin.ModelAdmin):
77+
class ProjectUserAttachmentLinkAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
7678
list_display = ("id", "owner", "title", "site_url")
7779
autocomplete_fields = ("owner",)
7880
search_fields = ("owner", "title", "stie_url")

apps/organizations/admin.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
from typing import Any, Optional
1+
from typing import Any
22

33
from django.conf import settings
44
from django.contrib import admin
55
from django.db.models import Count, QuerySet
66
from django.http.request import HttpRequest
77

8-
from apps.commons.admin import RoleBasedAccessAdmin
8+
from apps.commons.admin import RoleBasedAccessAdmin, TranslateObjectAdminMixin
99
from services.keycloak.interface import KeycloakService
1010

1111
from .exports import ProjectTemplateExportMixin
1212
from .models import Organization, ProjectCategory, Template, TemplateCategories
1313

1414

1515
@admin.register(Organization)
16-
class OrganizationAdmin(admin.ModelAdmin):
16+
class OrganizationAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
1717
list_display = (
1818
"code",
1919
"name",
@@ -54,7 +54,9 @@ def save_model(self, request, obj, form, change):
5454

5555

5656
@admin.register(Template)
57-
class TemplateAdmin(ProjectTemplateExportMixin, RoleBasedAccessAdmin):
57+
class TemplateAdmin(
58+
TranslateObjectAdminMixin, ProjectTemplateExportMixin, RoleBasedAccessAdmin
59+
):
5860
list_display = (
5961
"id",
6062
"display_organization",
@@ -81,7 +83,7 @@ def display_templates(self, instance: Template):
8183
@admin.display(
8284
description="Organization", ordering="categories__organization__name"
8385
)
84-
def display_organization(self, instance: Template) -> Optional[str]:
86+
def display_organization(self, instance: Template) -> str | None:
8587
names = [o.organization.name for o in instance.categories.all()]
8688
return " / ".join(set(names))
8789

@@ -95,7 +97,7 @@ def get_queryset_for_organizations(
9597

9698

9799
@admin.register(ProjectCategory)
98-
class ProjectCategoryAdmin(admin.ModelAdmin):
100+
class ProjectCategoryAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
99101
list_display = ("name", "display_templates")
100102
list_filter = ("name",)
101103

apps/projects/admin.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
from django.db.models import QuerySet
33
from import_export.admin import ExportActionMixin # type: ignore
44

5-
from apps.commons.admin import RoleBasedAccessAdmin
5+
from apps.commons.admin import RoleBasedAccessAdmin, TranslateObjectAdminMixin
66
from apps.organizations.models import Organization
77

88
from .exports import BlogEntryResource, ProjectResource
99
from .models import BlogEntry, Project
1010

1111

1212
@admin.register(Project)
13-
class ProjectAdmin(ExportActionMixin, RoleBasedAccessAdmin):
13+
class ProjectAdmin(TranslateObjectAdminMixin, ExportActionMixin, RoleBasedAccessAdmin):
1414
resource_classes = [ProjectResource, BlogEntryResource]
1515

1616
def get_queryset_for_organizations(
@@ -42,7 +42,9 @@ def get_queryset_for_organizations(
4242

4343

4444
@admin.register(BlogEntry)
45-
class BlogEntryAdmin(ExportActionMixin, RoleBasedAccessAdmin):
45+
class BlogEntryAdmin(
46+
TranslateObjectAdminMixin, ExportActionMixin, RoleBasedAccessAdmin
47+
):
4648
resource_classes = [BlogEntryResource]
4749

4850
def get_queryset_for_organizations(

apps/skills/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from django.urls import reverse
33
from django.utils.safestring import mark_safe
44

5+
from apps.commons.admin import TranslateObjectAdminMixin
6+
57
from .models import Skill, Tag, TagClassification
68

79

@@ -29,7 +31,7 @@ class TagAdmin(admin.ModelAdmin):
2931
)
3032

3133

32-
class TagClassificationAdmin(admin.ModelAdmin):
34+
class TagClassificationAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
3335
list_display = (
3436
"id",
3537
"slug",

services/crisalid/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.db.models import Count
55

66
from apps.accounts.models import ProjectUser
7+
from apps.commons.admin import TranslateObjectAdminMixin
78
from services.crisalid.tasks import vectorize_documents
89

910
from .models import (
@@ -44,7 +45,7 @@ class DocumentContributorAdminInline(admin.StackedInline):
4445

4546

4647
@admin.register(Document)
47-
class DocumentAdmin(admin.ModelAdmin):
48+
class DocumentAdmin(TranslateObjectAdminMixin, admin.ModelAdmin):
4849
list_display = (
4950
"title",
5051
"publication_date",

services/crisalid/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.db.models.functions import Lower
77

88
from apps.commons.mixins import OrganizationRelated
9+
from apps.organizations.models import Organization
910
from services.crisalid import relators
1011
from services.mistral.models import DocumentEmbedding
1112
from services.translator.mixins import HasAutoTranslatedFields
@@ -120,7 +121,7 @@ class Meta:
120121
]
121122

122123

123-
class Document(HasAutoTranslatedFields, CrisalidDataModel):
124+
class Document(OrganizationRelated, HasAutoTranslatedFields, CrisalidDataModel):
124125
"""
125126
Represents a research publicaiton (or 'document') in the Crisalid system.
126127
"""
@@ -196,6 +197,18 @@ class DocumentType(models.TextChoices):
196197
"crisalid.Identifier", related_name="documents"
197198
)
198199

200+
organization_query_string = "contributors__user__groups__organizations"
201+
202+
def get_related_organizations(self):
203+
"""organizations from user"""
204+
return list(
205+
Organization.objects.filter(
206+
id__in=self.contributors.all()
207+
.values_list("user__groups__organizations", flat=True)
208+
.distinct("id")
209+
)
210+
)
211+
199212
@property
200213
def document_type_centralized(self) -> list[str]:
201214
"""get group list document centralized"""

services/translator/tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
22

3+
from django.contrib.contenttypes.models import ContentType
4+
35
from apps.commons.utils import clear_memory
46
from projects.celery import app
57

@@ -17,3 +19,37 @@ def automatic_translations():
1719
update_auto_translated_field(field)
1820
except Exception as e: # noqa: PIE786
1921
logger.error(f"Error updating auto-translated field {field.id}: {e}")
22+
23+
24+
@app.task(name="apps.translations.tasks.translate_object")
25+
@clear_memory
26+
def translate_object(
27+
content_type_id: int, object_id: int, fields_name: list[str] | None = None
28+
):
29+
"""force retranslate all field from one models
30+
31+
:param content_type_id: content_type model id
32+
:param object_id: model id
33+
"""
34+
35+
model = ContentType.objects.get(id=content_type_id)
36+
queryset = AutoTranslatedField.objects.filter(
37+
content_type=model, object_id=object_id
38+
)
39+
if fields_name is not None:
40+
queryset = queryset.filter(field_name__in=fields_name)
41+
42+
logger.info(
43+
"Start translated model %r(id=%s) for fields %r",
44+
model,
45+
object_id,
46+
fields_name if fields_name is not None else "all",
47+
)
48+
49+
for field in queryset:
50+
try:
51+
update_auto_translated_field(field)
52+
except Exception as e: # noqa: PIE786
53+
logger.error(
54+
f"Error updating model-translated {model} field {field.id}: {e}"
55+
)

services/translator/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import re
2-
from typing import List, Union
32

43
from bs4 import BeautifulSoup
54

@@ -13,7 +12,7 @@
1312

1413
def split_content(
1514
content: str, max_length: int, text_type: str = "plain"
16-
) -> Union[List[str], List[BeautifulSoup]]:
15+
) -> list[str] | list[BeautifulSoup]:
1716
"""
1817
Split content into chunks of max_length, trying to split at html tags.
1918
@@ -59,9 +58,8 @@ def update_auto_translated_field(field: AutoTranslatedField):
5958
organizations = [
6059
o for o in instance.get_related_organizations() if o.auto_translate_content
6160
]
62-
languages = list(
63-
dict.fromkeys([lang for org in organizations for lang in org.languages])
64-
)
61+
# iter over languages in set (remove duplicate language)
62+
languages: set[str] = {lang for org in organizations for lang in org.languages}
6563
if languages:
6664
base_max_length = AZURE_MAX_LENGTH * 0.8 # Safety margin
6765
max_length = int(base_max_length // len(languages))

0 commit comments

Comments
 (0)