Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added: Add multi-languageBlockTranslatedContent in relation to Block #1227

Merged
merged 27 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
95a7426
feat: Add BlockTranslatedContent model and relate it to Block with a …
drikusroor Aug 7, 2024
adcea3e
refactor: Migrate block content and associated models
drikusroor Aug 8, 2024
1a6b588
refactor: Add `translated_content` as `ManyToMany` field of `Block` i…
drikusroor Aug 9, 2024
11cb523
feat: Warn user about missing translated contents
drikusroor Aug 21, 2024
c50f5b0
refactor: Remove hashtag, url, consent & language from block
drikusroor Aug 21, 2024
e64223c
refactor: Move warnings to remarks column
drikusroor Aug 21, 2024
34dc7af
fix: Fix many tests after removing consent/url/hashtags from block model
drikusroor Aug 22, 2024
409cb24
doc: Update consent's doc string
drikusroor Aug 22, 2024
1ddce0c
fix: Fix minor test
drikusroor Aug 22, 2024
6de8475
fix: Fix remainder of tests
drikusroor Aug 22, 2024
6ee07b6
fix: Validating the model before saving the model and its relations r…
drikusroor Aug 23, 2024
80d930a
refactor: Handle missing language in content
drikusroor Aug 23, 2024
84fd191
Merge branch 'develop' into feat/1215-block-translated-content
drikusroor Aug 28, 2024
b069e3c
fix: Re-add Hooked ID
drikusroor Aug 28, 2024
6cda4e1
refactor(`BlockTranslatedContent`) Refactor `Block`-`BlockTranslatedC…
drikusroor Aug 28, 2024
4731ea7
refactor: Use tabular inline for block translated content
drikusroor Aug 28, 2024
d7865c8
fix: Fix border radius top left in markdown input
drikusroor Aug 28, 2024
d0284d6
feat: Add all necessary fields for experiment translated content to i…
drikusroor Aug 28, 2024
a3adcf0
chore: Remove unnecessary print statement
drikusroor Aug 28, 2024
9c5bae0
fix: Add `BlockTranslatedContentInline` inline to `BlockAdmin`
drikusroor Aug 30, 2024
fd57274
fix: Create temporary fix for tabular inline headings appearing out o…
drikusroor Aug 30, 2024
2fbfd60
Merge branch 'develop' into feat/1215-block-translated-content
drikusroor Sep 2, 2024
e48d47a
Merge branch 'develop' into feat/1215-block-translated-content
drikusroor Sep 2, 2024
5947160
chore: Incorporate latest version of `django-nested-admin` that inclu…
drikusroor Sep 2, 2024
bbd686c
refactor: Rename test to be more descriptive
drikusroor Sep 2, 2024
811821b
refactor: Incorporate the migration of #1240
drikusroor Sep 4, 2024
0c00c6c
Merge branch 'develop' into feat/1215-block-translated-content
drikusroor Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions backend/experiment/actions/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.template.loader import render_to_string
from django.template import Template, Context
from django_markup.markup import formatter
from django.core.files import File

from .base_action import BaseAction

Expand All @@ -11,13 +12,13 @@ def get_render_format(url: str) -> str:
"""
Detect markdown file based on file extension
"""
if splitext(url)[1] == '.md':
return 'MARKDOWN'
return 'HTML'
if splitext(url)[1] == ".md":
return "MARKDOWN"
return "HTML"


def render_html_or_markdown(dry_text: str, render_format: str) -> str:
'''
"""
render html or markdown

Parameters:
Expand All @@ -26,19 +27,19 @@ def render_html_or_markdown(dry_text: str, render_format: str) -> str:

Returns:
a string of content rendered to html
'''
if render_format == 'HTML':
"""
if render_format == "HTML":
template = Template(dry_text)
context = Context()
return template.render(context)
if render_format == 'MARKDOWN':
return formatter(dry_text, filter_name='markdown')
if render_format == "MARKDOWN":
return formatter(dry_text, filter_name="markdown")


class Consent(BaseAction): # pylint: disable=too-few-public-methods
"""
Provide data for a view that ask consent for using the experiment data
- text: Uploaded file via block.consent (fileField)
- text: Uploaded file via an experiment's translated content's consent (fileField)
- title: The title to be displayed
- confirm: The text on the confirm button
- deny: The text on the deny button
Expand All @@ -50,7 +51,7 @@ class Consent(BaseAction): # pylint: disable=too-few-public-methods
"""

# default consent text, that can be used for multiple blocks
ID = 'CONSENT'
ID = "CONSENT"

default_text = "Lorem ipsum dolor sit amet, nec te atqui scribentur. Diam \
molestie posidonium te sit, ea sea expetenda suscipiantur \
Expand All @@ -63,21 +64,21 @@ class Consent(BaseAction): # pylint: disable=too-few-public-methods
amet, nec te atqui scribentur. Diam molestie posidonium te sit, \
ea sea expetenda suscipiantur contentiones."

def __init__(self, text, title='Informed consent', confirm='I agree', deny='Stop', url=''):
def __init__(self, text: File, title="Informed consent", confirm="I agree", deny="Stop", url=""):
# Determine which text to use
if text!='':
if text != "":
# Uploaded consent via file field: block.consent (High priority)
with text.open('r') as f:
with text.open("r") as f:
dry_text = f.read()
render_format = get_render_format(text.url)
elif url!='':
elif url != "":
# Template file via url (Low priority)
dry_text = render_to_string(url)
render_format = get_render_format(url)
else:
# use default text
dry_text = self.default_text
render_format = 'HTML'
render_format = "HTML"
# render text fot the consent component
self.text = render_html_or_markdown(dry_text, render_format)
self.title = title
Expand Down
97 changes: 77 additions & 20 deletions backend/experiment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,35 @@
from io import BytesIO

from django.conf import settings
from django.contrib import admin
from django.contrib import admin, messages
from django.db import models
from django.utils import timezone
from django.core import serializers
from django.shortcuts import render, redirect
from django.forms import CheckboxSelectMultiple
from django.http import HttpResponse

from inline_actions.admin import InlineActionsModelAdminMixin
from django.urls import reverse
from django.utils.html import format_html

from nested_admin import NestedModelAdmin, NestedStackedInline, NestedTabularInline

from experiment.utils import check_missing_translations, get_flag_emoji, get_missing_content_blocks

from experiment.models import (
Block,
Experiment,
Phase,
Feedback,
SocialMediaConfig,
ExperimentTranslatedContent,
BlockTranslatedContent,
)
from question.admin import QuestionSeriesInline
from experiment.forms import (
ExperimentForm,
ExperimentTranslatedContentForm,
BlockForm,
ExportForm,
TemplateForm,
Expand All @@ -46,9 +51,19 @@ class FeedbackInline(admin.TabularInline):
extra = 0


class BlockTranslatedContentInline(NestedTabularInline):
model = BlockTranslatedContent

def get_extra(self, request, obj=None, **kwargs):
if obj:
return 0
return 1


class ExperimentTranslatedContentInline(NestedStackedInline):
model = ExperimentTranslatedContent
sortable_field_name = "index"
form = ExperimentTranslatedContentForm

def get_extra(self, request, obj=None, **kwargs):
if obj:
Expand All @@ -75,18 +90,14 @@ class BlockAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin):
"description",
"image",
"slug",
"url",
"hashtag",
"theme_config",
"language",
"active",
"rules",
"rounds",
"bonus_points",
"playlists",
"consent",
]
inlines = [QuestionSeriesInline, FeedbackInline]
inlines = [QuestionSeriesInline, FeedbackInline, BlockTranslatedContentInline]
form = BlockForm

# make playlists fields a list of checkboxes
Expand Down Expand Up @@ -232,6 +243,8 @@ def block_slug_link(self, obj):
class BlockInline(NestedStackedInline):
model = Block
sortable_field_name = "index"
inlines = [BlockTranslatedContentInline]
form = BlockForm

def get_extra(self, request, obj=None, **kwargs):
if obj:
Expand Down Expand Up @@ -289,6 +302,7 @@ class Media:

def name(self, obj):
content = obj.get_fallback_content()

return content.name if content else "No name"

def slug_link(self, obj):
Expand All @@ -300,17 +314,15 @@ def slug_link(self, obj):
)

def description_excerpt(self, obj):
fallback_content = obj.get_fallback_content()
description = (
fallback_content.description if fallback_content and fallback_content.description else "No description"
)
experiment_fallback_content = obj.get_fallback_content()
description = experiment_fallback_content.description if experiment_fallback_content else "No description"
if len(description) < 50:
return description

return description[:50] + "..."

def phases(self, obj):
phases = Phase.objects.filter(series=obj)
phases = Phase.objects.filter(experiment=obj)
return format_html(
", ".join([f'<a href="/admin/experiment/phase/{phase.id}/change/">{phase.name}</a>' for phase in phases])
)
Expand Down Expand Up @@ -382,13 +394,21 @@ def remarks(self, obj):
}
)

if not remarks_array:
remarks_array.append({"level": "success", "message": "✅ All good", "title": "No issues found."})

supported_languages = obj.translated_content.values_list("language", flat=True).distinct()

# TODO: Check if all blocks support the same languages as the experiment
# Implement this when the blocks have been updated to support multiple languages
missing_content_block_translations = check_missing_translations(obj)

if missing_content_block_translations:
remarks_array.append(
{
"level": "warning",
"message": "🌍 Missing block content",
"title": missing_content_block_translations,
}
)

if not remarks_array:
remarks_array.append({"level": "success", "message": "✅ All good", "title": "No issues found."})

# TODO: Check if all theme configs support the same languages as the experiment
# Implement this when the theme configs have been updated to support multiple languages
Expand All @@ -399,12 +419,28 @@ def remarks(self, obj):
return format_html(
"\n".join(
[
f'<span class="badge badge-{remark["level"]} whitespace-nowrap text-xs mt-1" title="{remark.get("title") if remark.get("title") else remark["message"]}">{remark["message"]}</span>'
f'<span class="badge badge-{remark["level"]} whitespace-nowrap text-xs mt-1" title="{remark.get("title") if remark.get("title") else remark["message"]}">{remark["message"]}</span><br>'
for remark in remarks_array
]
)
)

def save_model(self, request, obj, form, change):
# Save the model
super().save_model(request, obj, form, change)

# Check for missing translations after saving
missing_content_blocks = get_missing_content_blocks(obj)

if missing_content_blocks:
for block, missing_languages in missing_content_blocks:
missing_language_flags = [get_flag_emoji(language) for language in missing_languages]
self.message_user(
request,
f"Block {block.name} does not have content in {', '.join(missing_language_flags)}",
level=messages.WARNING,
)


admin.site.register(Experiment, ExperimentAdmin)

Expand All @@ -418,7 +454,7 @@ class PhaseAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin):
"randomize",
"blocks",
)
fields = ["name", "series", "index", "dashboard", "randomize"]
fields = ["name", "experiment", "index", "dashboard", "randomize"]
inlines = [BlockInline]

def name_link(self, obj):
Expand All @@ -427,8 +463,8 @@ def name_link(self, obj):
return format_html('<a href="{}">{}</a>', url, obj_name)

def related_experiment(self, obj):
url = reverse("admin:experiment_experiment_change", args=[obj.series.pk])
content = obj.series.get_fallback_content()
url = reverse("admin:experiment_experiment_change", args=[obj.experiment.pk])
content = obj.experiment.get_fallback_content()
experiment_name = content.name if content else "No name"
return format_html('<a href="{}">{}</a>', url, experiment_name)

Expand All @@ -444,3 +480,24 @@ def blocks(self, obj):


admin.site.register(Phase, PhaseAdmin)


@admin.register(BlockTranslatedContent)
class BlockTranslatedContentAdmin(admin.ModelAdmin):
list_display = ["name", "block", "language"]
list_filter = ["language"]
search_fields = [
"name",
"block__name",
]

def blocks(self, obj):
# Block is manytomany, so we need to find it through the related name
blocks = Block.objects.filter(translated_contents=obj)

if not blocks:
return "No block"

return format_html(
", ".join([f'<a href="/admin/experiment/block/{block.id}/change/">{block.name}</a>' for block in blocks])
)
Loading
Loading