-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d1abaf6
commit 5f7fcd8
Showing
27 changed files
with
1,025 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
from urllib.parse import quote as urlquote | ||
|
||
import reversion | ||
from django.contrib import admin, messages | ||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters | ||
from django.contrib.admin.utils import unquote | ||
from django.db import models | ||
from django.http import HttpResponseRedirect | ||
from django.shortcuts import get_object_or_404 | ||
from django.utils.html import format_html | ||
from django.utils.translation import gettext as _ | ||
from localized_fields.admin import LocalizedFieldsAdminMixin | ||
from martor.widgets import AdminMartorWidget | ||
from rest_framework import status | ||
from reversion import create_revision | ||
from reversion.admin import VersionAdmin | ||
from reversion.models import Version | ||
|
||
from headless_cms.settings import headless_cms_settings | ||
|
||
|
||
class PublishStatusInlineMixin: | ||
show_change_link = True | ||
|
||
readonly_fields = ("publish_status",) | ||
|
||
def publish_status(self, obj): | ||
published_state = "unpublished" | ||
if obj.published_version: | ||
last_ver = Version.objects.get_for_object(obj).first() | ||
if last_ver.id == obj.published_version.id: | ||
published_state = "published (latest)" | ||
else: | ||
published_state = "published (outdated)" | ||
return published_state | ||
|
||
|
||
@admin.action(description="Publish selected") | ||
def publish(modeladmin, request, queryset): | ||
for obj in queryset.all(): | ||
with reversion.create_revision(): | ||
reversion.set_comment("Publish") | ||
reversion.set_user(request.user) | ||
|
||
obj.save() | ||
|
||
with reversion.create_revision(manage_manually=True): | ||
last_ver = Version.objects.get_for_object(obj).first() | ||
|
||
obj.published_version = last_ver | ||
obj.save() | ||
|
||
|
||
class EnhancedLocalizedVersionAdmin(LocalizedFieldsAdminMixin, VersionAdmin): | ||
actions = [publish] | ||
formfield_overrides = { | ||
models.TextField: {"widget": AdminMartorWidget}, | ||
} | ||
|
||
def get_list_display(self, request): | ||
list_display = super().get_list_display(request) | ||
return list_display + ("published_state",) | ||
|
||
def render_change_form(self, request, context, *args, **kwargs): | ||
"""We need to update the context to show the button.""" | ||
obj = kwargs.get("obj") | ||
if obj and not context.get("revert"): | ||
show_publish = True | ||
show_unpublish = False | ||
|
||
published_state = "unpublished" | ||
if obj.published_version: | ||
last_ver = Version.objects.get_for_object(obj).first() | ||
show_unpublish = True | ||
if last_ver.id == obj.published_version.id: | ||
published_state = "published (latest)" | ||
show_publish = False | ||
else: | ||
published_state = "published (outdated)" | ||
|
||
context.update( | ||
{ | ||
"published_state": published_state, | ||
"show_publish": show_publish, | ||
"show_unpublish": show_unpublish, | ||
"show_translate": True, | ||
} | ||
) | ||
|
||
return super().render_change_form(request, context, *args, **kwargs) | ||
|
||
def changelist_view(self, request, extra_context=None): | ||
if request.POST and "action" in request.POST: | ||
context = { | ||
"has_change_permission": self.has_change_permission(request), | ||
} | ||
context.update(extra_context or {}) | ||
return super(VersionAdmin, self).changelist_view(request, context) | ||
else: | ||
return super().changelist_view(request, extra_context) | ||
|
||
def change_view(self, request, object_id, form_url="", extra_context=None): | ||
res = super().change_view(request, object_id, form_url, extra_context) | ||
|
||
if "_publish" in request.POST: | ||
with reversion.create_revision(manage_manually=True): | ||
obj = self.get_object(request, unquote(object_id)) | ||
last_ver = Version.objects.get_for_object(obj).first() | ||
|
||
obj.published_version = last_ver | ||
obj.save() | ||
elif "_unpublish" in request.POST: | ||
with reversion.create_revision(manage_manually=True): | ||
obj = self.get_object(request, unquote(object_id)) | ||
obj.published_version = None | ||
obj.save() | ||
return res | ||
|
||
def response_change(self, request, obj): | ||
opts = self.opts | ||
preserved_filters = self.get_preserved_filters(request) | ||
|
||
msg_dict = { | ||
"name": opts.verbose_name, | ||
"obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj), | ||
} | ||
if "_publish" in request.POST: | ||
reversion.set_comment("Publish") | ||
|
||
msg = format_html( | ||
_("The {name} “{obj}” was published successfully."), | ||
**msg_dict, | ||
) | ||
self.message_user(request, msg, messages.SUCCESS) | ||
redirect_url = request.path | ||
redirect_url = add_preserved_filters( | ||
{"preserved_filters": preserved_filters, "opts": opts}, redirect_url | ||
) | ||
return HttpResponseRedirect(redirect_url) | ||
elif "_unpublish" in request.POST: | ||
reversion.set_comment("Unpublished") | ||
|
||
msg = format_html( | ||
_("The {name} “{obj}” was unpublished."), | ||
**msg_dict, | ||
) | ||
self.message_user(request, msg, messages.SUCCESS) | ||
redirect_url = request.path | ||
redirect_url = add_preserved_filters( | ||
{"preserved_filters": preserved_filters, "opts": opts}, redirect_url | ||
) | ||
return HttpResponseRedirect(redirect_url) | ||
elif "_translate" in request.POST or "_force_translate" in request.POST: | ||
force = "_force_translate" in request.POST | ||
reversion.set_comment(f"Object translated{' (forced)' if force else ''}.") | ||
|
||
translator = headless_cms_settings.AUTO_TRANSLATE_CLASS(obj) | ||
translator.process(force=force) | ||
|
||
msg = format_html( | ||
_( | ||
f"The {{name}} “{{obj}}” was translated{' (forced)' if force else ''}." | ||
), | ||
**msg_dict, | ||
) | ||
self.message_user(request, msg, messages.SUCCESS) | ||
redirect_url = request.path | ||
redirect_url = add_preserved_filters( | ||
{"preserved_filters": preserved_filters, "opts": opts}, redirect_url | ||
) | ||
return HttpResponseRedirect(redirect_url) | ||
else: | ||
# Otherwise, use default behavior | ||
return super().response_change(request, obj) | ||
|
||
def revision_view(self, request, object_id, version_id, extra_context=None): | ||
"""Displays the contents of the given revision.""" | ||
object_id = unquote(object_id) # Underscores in primary key get quoted to "_5F" | ||
version = get_object_or_404(Version, pk=version_id, object_id=object_id) | ||
context = { | ||
"title": _("Revert %(name)s") % {"name": version.object_repr}, | ||
"revert": True, | ||
"is_published": ( | ||
version.object.published_version | ||
and version.revision_id == version.object.published_version.revision_id | ||
), | ||
} | ||
context.update(extra_context or {}) | ||
return self._reversion_revisionform_view( | ||
request, | ||
version, | ||
self.revision_form_template | ||
or self._reversion_get_template_list("revision_form.html"), | ||
context, | ||
) | ||
|
||
def _reversion_revisionform_view( | ||
self, request, version, template_name, extra_context=None | ||
): | ||
old_published_version = ( | ||
version.object.published_version if version.object else None | ||
) | ||
res = super()._reversion_revisionform_view( | ||
request, version, template_name, extra_context | ||
) | ||
|
||
if res.status_code == status.HTTP_302_FOUND: | ||
if old_published_version: | ||
with create_revision(manage_manually=True): | ||
version.refresh_from_db() | ||
version.object.published_version_id = old_published_version | ||
version.object.save() | ||
|
||
return res |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .base_translate import BaseTranslate | ||
|
||
__all__ = ["BaseTranslate"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from django.conf import settings | ||
from django.contrib.contenttypes.fields import GenericRelation | ||
from localized_fields.fields import LocalizedField | ||
from localized_fields.models import LocalizedModel | ||
from localized_fields.value import LocalizedValue | ||
|
||
|
||
class BaseTranslate: | ||
"""Class to use when click on admin translation buttons. | ||
Attributes: | ||
can_batch_translate: Indicates whether the inherited class can translate multiple languages | ||
in batch or not for improving the processing time. | ||
""" | ||
|
||
can_batch_translate = False | ||
|
||
def __init__(self, instance: LocalizedModel): | ||
self.instance = instance | ||
self.fields = instance._meta.get_fields() | ||
|
||
def translate(self, language: str, text: str): | ||
"""Override this function to translate text to a single language""" | ||
return text | ||
|
||
def batch_translate(self, languages: list[str], text: str): | ||
"""Override this function to translate text into multiple languages""" | ||
raise NotImplementedError | ||
|
||
def _handle_translate(self, field_value: LocalizedValue, lang: str, text: str): | ||
translated_value = self.translate(lang, text) | ||
field_value.set(lang, translated_value) | ||
|
||
def _handle_batch_translate( | ||
self, field_value: LocalizedValue, text: str, force: bool | ||
): | ||
langs_to_translate = [] | ||
for lang, _lang_name in settings.LANGUAGES: | ||
if lang == settings.LANGUAGE_CODE: | ||
continue | ||
if not force and getattr(field_value, lang): | ||
continue | ||
langs_to_translate.append(lang) | ||
translated_texts = self.batch_translate(langs_to_translate, text) | ||
if len(translated_texts) != len(langs_to_translate): | ||
return | ||
|
||
for lang, translated_text in zip( | ||
langs_to_translate, translated_texts, strict=True | ||
): | ||
field_value.set(lang, translated_text) | ||
|
||
def process(self, force=False): | ||
"""Call this one to process the translation for the database object instance, it will translate | ||
all localized fields and recursive calls this one to the child localized models too. | ||
Arguments: | ||
force: whether to force retranslation for all localized fields(even | ||
the fields are already translated). | ||
""" | ||
for field in self.fields: | ||
if isinstance(field, LocalizedField): | ||
field_value = getattr(self.instance, field.name) | ||
base_value = getattr(field_value, settings.LANGUAGE_CODE) | ||
if base_value: | ||
if self.can_batch_translate: | ||
self._handle_batch_translate(field_value, base_value, force) | ||
else: | ||
for lang, _lang_name in settings.LANGUAGES: | ||
if lang == settings.LANGUAGE_CODE: | ||
continue | ||
if not force and getattr(field_value, lang): | ||
continue | ||
|
||
self._handle_translate(field_value, lang, base_value) | ||
elif isinstance(field, GenericRelation) and issubclass( | ||
field.related_model, LocalizedModel | ||
): | ||
sub_items = getattr(self.instance, field.name).all() | ||
for item in sub_items: | ||
self.__class__(item).process(force) | ||
self.instance.save() |
Empty file.
49 changes: 49 additions & 0 deletions
49
headless_cms/enhances/static/localized_fields/localized-fields-admin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
(function($) { | ||
var syncTabs = function(lang) { | ||
$('.localized-fields-widget.tab label:contains("'+lang+'")').each(function(){ | ||
$(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active'); | ||
$(this).parents('.localized-fields-widget.tab').addClass('active'); | ||
$(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget>[role="tabpanel"]').hide(); | ||
$('#'+$(this).attr('for')).show(); | ||
}); | ||
} | ||
|
||
$(function (){ | ||
if (window.sessionStorage) { | ||
var lang = window.sessionStorage.getItem('localized-field-lang'); | ||
|
||
$(window).on("load", function () { | ||
if (lang) { | ||
syncTabs(lang); | ||
} | ||
|
||
}); | ||
} | ||
|
||
$(window).on("load", function () { | ||
if (window.sessionStorage) { | ||
var lang = window.sessionStorage.getItem('localized-field-lang'); | ||
|
||
if (lang) { | ||
syncTabs(lang); | ||
return | ||
} | ||
} | ||
$('.localized-fields-widget>[role="tabpanel"]').hide(); | ||
|
||
$('.localized-fields-widget[role="tabs"]').each(function () { | ||
$(this).find('.localized-fields-widget.tab:first').addClass('active'); | ||
$('#'+$(this).find('.localized-fields-widget.tab:first label').attr('for')).show(); | ||
}); | ||
}); | ||
|
||
$('.localized-fields-widget.tab label').click(function(event) { | ||
event.preventDefault(); | ||
syncTabs(this.innerText); | ||
if (window.sessionStorage) { | ||
window.sessionStorage.setItem('localized-field-lang', this.innerText); | ||
} | ||
return false; | ||
}); | ||
}); | ||
})(django.jQuery) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{% extends 'admin/change_form.html' %} | ||
{% load i18n %} | ||
|
||
{% block object-tools %} | ||
{{ block.super }} | ||
{% if published_state %} | ||
<div style="flex-basis: 100%">Item <b>{{ published_state }}</b>.</div> | ||
{% endif %} | ||
{% endblock %} | ||
|
||
{% block field_sets %} | ||
{% if published_state %} | ||
<div class="submit-row"> | ||
{% if show_publish %} | ||
<input class="default" type="submit" value="{% translate 'Publish' %}" name="_publish">{% endif %} | ||
{% if show_unpublish %}<input style="background-color: var(--delete-button-bg)" type="submit" | ||
value="{% translate 'Unpublish' %}" name="_unpublish">{% endif %} | ||
</div> | ||
{% endif %} | ||
{{ block.super }} | ||
{% endblock %} | ||
|
||
{% block after_related_objects %} | ||
{{ block.super }} | ||
{% if show_translate %} | ||
<div class="submit-row"> | ||
<input class="default" type="submit" value="{% translate 'Translate missing' %}" name="_translate"> | ||
<input class="default" type="submit" value="{% translate 'Force re-translate' %}" name="_force_translate"> | ||
</div> | ||
{% endif %} | ||
{% endblock %} |
1 change: 1 addition & 0 deletions
1
headless_cms/enhances/templates/adminsortable2/edit_inline/stacked-django-5.0.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{% extends 'adminsortable2/edit_inline/stacked-django-4.2.html' %} |
1 change: 1 addition & 0 deletions
1
headless_cms/enhances/templates/adminsortable2/edit_inline/tabular-django-5.0.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{% extends 'adminsortable2/edit_inline/tabular-django-4.2.html' %} |
Oops, something went wrong.