Skip to content

Commit

Permalink
[f] Implement headless cms
Browse files Browse the repository at this point in the history
  • Loading branch information
huynguyengl99 committed Mar 30, 2024
1 parent d1abaf6 commit 5bc4709
Show file tree
Hide file tree
Showing 27 changed files with 1,023 additions and 0 deletions.
213 changes: 213 additions & 0 deletions headless_cms/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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.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 reversion.create_revision(manage_manually=True):
version.refresh_from_db()
version.object.published_version_id = old_published_version
version.object.save()

return res
3 changes: 3 additions & 0 deletions headless_cms/auto_translate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .base_translate import BaseTranslate

__all__ = ["BaseTranslate"]
82 changes: 82 additions & 0 deletions headless_cms/auto_translate/base_translate.py
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.
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)
31 changes: 31 additions & 0 deletions headless_cms/enhances/templates/admin/change_form.html
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 %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends 'adminsortable2/edit_inline/stacked-django-4.2.html' %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends 'adminsortable2/edit_inline/tabular-django-4.2.html' %}
Loading

0 comments on commit 5bc4709

Please sign in to comment.