Skip to content
Snippets Groups Projects
Commit 8f20aa3c authored by borzechof99's avatar borzechof99 :whale2:
Browse files

Merge branch '37-add-internationalization-to-models' into 'master'

Resolve "Add Internationalization to Models"

Closes #37

See merge request swp-unisport/team-warumkeinrust/unisport-o-mat!43
parents a868600e 3ad0e8d9
No related branches found
No related tags found
No related merge requests found
......@@ -86,3 +86,31 @@ If you started the server as described above, you can access the django admin in
[localhost:8000/admin](localhost:8000/admin).
If you seeded the database you can login with username: "admin" and the password you specified.
## Internationalization and how to use it
At the current time, the backend uses [django-modeltranslation](https://django-modeltranslation.readthedocs.io/en/latest/index.html) to handle translations of Strings in the models Question, CallToMove, KnowledgeSnack. The list of available languages is defined in `settings.py`, and the translatable fields are defined in `translation.py`. Right now, the two languages `de` and `en` are enabled, with `de` translations needing to be filled out.
Django internally keeps track of the active language, which decides on the language strings that `object.text` returns. If you want to force a certain language, `object.text_de` can be used.
`"django.middleware.locale.LocaleMiddleware"` has been added as a Middleware. This allows Django to recognize the locale of the requesting Browser and automatically sets the language to that locale. This means that both the Django Admin Panel and our own API choose the Strings depending on the GET request. It is yet to be determined whether React can change the locale in the requests, but if that were the case, this feature would potentially ease the implementation of Serializers for the User-Frontend.
Instead of relying on the Locale given by the Requests, one can also manually change the active language. This example demonstrates how:
```python
from django.utils.translation import get_language, activate
cur_language = get_language() # Returns current active language
activate("de") # Selects German as active language
print(object.text) # Prints German Translation
activate("en") # Selects English as active language
print(object.text) # Prints English Translation
activate(cur_language) # Resets active language to the one before manual activations
```
This might be particularly useful for entering data from the Admin Frontend into the database, or choosing the language for the User Frontend if the Browser Locale cannot be used.
\ No newline at end of file
......@@ -12,3 +12,4 @@ urllib3==1.26.4
Pillow==8.2.0
django-cors-headers==3.7.0
djangorestframework==3.12.4
django-modeltranslation==0.17.2
\ No newline at end of file
""" Here is the place to register the Models to be seen in the admin interface """
from django.contrib import admin
from modeltranslation.admin import TranslationAdmin
# Register your models here.
from .models import (
......@@ -12,9 +13,23 @@ from .models import (
KnowledgeSnack,
)
class QuestionAdmin(TranslationAdmin):
"""Allows for proper formatting of Translations in Question"""
class CallToMoveAdmin(TranslationAdmin):
"""Allows for proper formatting of Translations in CallToMove"""
class KnowledgeSnackAdmin(TranslationAdmin):
"""Allows for proper formatting of Translations in KnowledgeSnack"""
admin.site.register(Sport)
admin.site.register(Criterion)
admin.site.register(Question)
admin.site.register(CriterionRating)
admin.site.register(CallToMove)
admin.site.register(KnowledgeSnack)
admin.site.register(Question, QuestionAdmin)
admin.site.register(CallToMove, CallToMoveAdmin)
admin.site.register(KnowledgeSnack, KnowledgeSnackAdmin)
......@@ -8,6 +8,7 @@ import random
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.utils.translation import activate
from quiz.models import (
Sport,
Criterion,
......@@ -50,7 +51,7 @@ class Command(BaseCommand):
help="No super user shall be created",
)
def handle(self, *args, **options):
def handle(self, *args, **options): # pylint: disable=too-many-locals
"""Create some objects for all models"""
# delete all present database entries (necessary because of unique constraints)
......@@ -93,21 +94,43 @@ class Command(BaseCommand):
# Create questions
questions = [
"Ich würde am liebsten gemeinsam mit anderen trainieren.",
"Teamgeist und Wir-Gefühl sind für mich beim Sport eine große Motivation.",
"Ich betreibe lieber alleine Sport.",
"Ich bin bereit, mir ggf. Material für die Sportart zu kaufen.",
"Ich bevorzuge das Sportangebot draußen in der Natur vor dem Indoor-Angebot.",
(
"Ich würde am liebsten gemeinsam mit anderen trainieren.",
"I'd prefer to train with others.",
),
(
"Teamgeist und Wir-Gefühl sind für mich beim Sport eine große Motivation.",
"Being in a Team is a big motivation for me.",
),
("Ich betreibe lieber alleine Sport.", "I prefer to do sport alone"),
(
"Ich bin bereit, mir ggf. Material für die Sportart zu kaufen.",
"I am willing to buy extra materials for sport.",
),
(
"Ich bevorzuge das Sportangebot draußen in der Natur vor dem Indoor-Angebot.",
"I prefer sports outside in the nature instead of indoors.",
),
]
for number, criterion in enumerate(Criterion.objects.all()):
Question(text=questions[number], criterion=criterion).save()
activate("de")
que = Question(text=questions[number][0], criterion=criterion)
activate("en")
que.text = questions[number][1]
que.save()
# Create Calls to Move
calls_to_move = [
"Kreise deine Schultern vor der nächsten Frage 3x nach hinten",
"Stehe auf, beuge dich mit gestrecktem Rücken nach vorne und greife deinen Stuhl.",
"Mache vor der nächsten Frage 3 Jumping Jacks",
(
"Kreise deine Schultern vor der nächsten Frage 3x nach hinten",
"Move your shoulders in circles. Three Times. Noooow.",
),
(
"Stehe auf, beuge dich mit gestrecktem Rücken nach vorne und greife deinen Stuhl.",
"Stand up, keep your back straight, bow down.",
),
("Mache vor der nächsten Frage 3 Jumping Jacks", "Do Three Jumping Jacks."),
]
image = SimpleUploadedFile(
name="test_image.png",
......@@ -116,13 +139,26 @@ class Command(BaseCommand):
)
for text in calls_to_move:
CallToMove(text=text, image=image).save()
activate("de")
c_t_m = CallToMove(text=text[0], image=image)
activate("en")
c_t_m.text = text[1]
c_t_m.save()
# Create Knowledge Snacks
knowledge_snacks = [
"Dass Treppensteigen fast 5x so viele Kalorien verbrennt, wie das Nutzen des Aufzuges?",
"Dass das Spielemobil zur Mittagszeit immer auf dem Campus unterwegs ist?",
"Dass regelmäßige Bewegung Herz-Kreislauf-Erkrankungen vorbeugt?",
(
"Dass Treppensteigen fast 5x so viele Kalorien verbrennt, wie das Nutzen des Aufzuges?",
"That Taking the stairs burns five times as much calories than using the lift?",
),
(
"Dass das Spielemobil zur Mittagszeit immer auf dem Campus unterwegs ist?",
"That the Spielemobil is on campus every noon?",
),
(
"Dass regelmäßige Bewegung Herz-Kreislauf-Erkrankungen vorbeugt?",
"That proper training prevents heart disease?",
),
]
image = SimpleUploadedFile(
name="logo.png",
......@@ -130,4 +166,8 @@ class Command(BaseCommand):
content_type="image/png",
)
for text in knowledge_snacks:
KnowledgeSnack(text=text, image=image).save()
activate("de")
k_s = KnowledgeSnack(text=text[0], image=image)
activate("en")
k_s.text = text[1]
k_s.save()
# Generated by Django 3.2 on 2021-06-12 12:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0005_merge_20210602_1355"),
]
operations = [
migrations.AddField(
model_name="calltomove",
name="text_de",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="calltomove",
name="text_en",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="knowledgesnack",
name="text_de",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="knowledgesnack",
name="text_en",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="question",
name="text_de",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="question",
name="text_en",
field=models.TextField(null=True),
),
]
......@@ -2,7 +2,7 @@
Serializers creating JSONs for every Model from .models
"""
from rest_framework import serializers
from .models import Sport, Criterion
from .models import Sport, Criterion, Question
class SportListSerializer(serializers.ModelSerializer):
......@@ -12,7 +12,18 @@ class SportListSerializer(serializers.ModelSerializer):
class Meta:
model = Sport
fields = ("id", "name", "url", "criteria_ratings")
fields = ("pk", "name", "url", "criteria_ratings")
class QuestionListSerializer(serializers.ModelSerializer):
"""
Serializes all Questions.
"""
class Meta:
model = Question
fields = ("pk", "text", "criterion")
class CriterionListSerializer(serializers.ModelSerializer):
......@@ -22,4 +33,4 @@ class CriterionListSerializer(serializers.ModelSerializer):
class Meta:
model = Criterion
fields = ("id", "name")
fields = ("pk", "name")
......@@ -6,6 +6,7 @@ import tempfile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.utils.translation import get_language, activate
from django.test import TestCase, override_settings
from django.conf import settings
from .models import (
......@@ -276,3 +277,105 @@ class SeedingTest(TestCase):
self.assertEqual(Question.objects.count(), n_criteria)
self.assertEqual(CallToMove.objects.count(), 3)
self.assertEqual(KnowledgeSnack.objects.count(), 3)
class ModeltranslationFallbackTest(TestCase):
"""
Tests Behaviour of Modeltranslation when no translation is given
Also tests Default Language
"""
def setUp(self):
"""
Creates a Question and fills the german Translation
"""
self.criterion = Criterion(name="test")
self.question = Question(text="", criterion=self.criterion)
self.criterion.save()
self.question.save()
def test_default_language(self):
"""
Checks whether the Default Language is German
"""
cur_language = get_language()
self.assertEqual(cur_language, "de")
def test_vallback_value(self):
"""
Checks whether if no Translation is set, the Fallbackvalue is used
"""
self.assertEqual(self.question.text, ("No Translation for this Field",))
class Modeltranslation_One_Language_Test(TestCase):
"""
Tests Behaviour when only one language is defined
"""
def setUp(self):
"""
Creates a Question and fills the german Translation
"""
self.criterion = Criterion(name="test")
self.question = Question(text="", criterion=self.criterion)
self.question.text = "de_text"
self.criterion.save()
self.question.save()
def test_german_translation(self):
"""
Check whether obj.text returns the German Translation
"""
self.assertEqual(self.question.text, "de_text")
def test_fallback_value_translations(self):
"""
English Translation is not filled out, check whether german text is returned
"""
activate("en")
self.assertNotEqual(self.question.text, self.question.text_en)
self.assertEqual(self.question.text, self.question.text_de)
class Modeltranslation_Two_Languages_Test(TestCase):
"""
Tests Behaviour when two languages are defined
"""
def setUp(self):
"""
Creates a Question and fills both the german and english Translations
"""
self.criterion = Criterion(name="test")
self.question = Question(text="", criterion=self.criterion)
activate("de")
self.question.text = "de_text"
activate("en")
self.question.text = "en_text"
activate("de")
self.criterion.save()
self.question.save()
def test_german_translation(self):
"""
Tests whether German Translation is returned
"""
activate("de")
self.assertEqual(self.question.text, self.question.text_de)
self.assertNotEqual(self.question.text, self.question.text_en)
def test_english_translation(self):
"""
Tests whether English Translation is returned
"""
activate("en")
self.assertEqual(self.question.text, self.question.text_en)
self.assertNotEqual(self.question.text, self.question.text_de)
"""
Here, every Model which needs translation fields is registered.
"""
from modeltranslation.translator import register, TranslationOptions
from .models import Question, CallToMove, KnowledgeSnack
@register(Question)
class QuestionTranslationOptions(TranslationOptions):
"""
Translations for Question-model. Only the text of the question needs to be translated.
A German Translation is Required.
"""
fields = ("text",)
required_languages = ("de",)
fallback_values = ("No Translation for this Field",)
@register(CallToMove)
class CallToMoveTranslationOptions(TranslationOptions):
"""
Translations for CallToMove-model. Only the text of the call to move needs to be translated.
A German Translation is Required.
"""
fields = ("text",)
required_languages = ("de",)
fallback_values = ("No Translation for this Field",)
@register(KnowledgeSnack)
class KnowledgeSnackTranslationOptions(TranslationOptions):
"""
Translations for KnowledgeSnack-model. Only the text of the knowledge snack needs to be translated.
A German Translation is Required.
"""
fields = ("text",)
required_languages = ("de",)
fallback_values = ("No Translation for this Field",)
......@@ -4,8 +4,12 @@ Defines the views for the API
# from django.shortcuts import render
from rest_framework import viewsets
from .serializers import SportListSerializer, CriterionListSerializer
from .models import Sport, Criterion
from .serializers import (
SportListSerializer,
CriterionListSerializer,
QuestionListSerializer,
)
from .models import Sport, Criterion, Question
# Create your views here.
......@@ -26,3 +30,12 @@ class CriterionListView(viewsets.ModelViewSet): # pylint: disable=too-many-ance
serializer_class = CriterionListSerializer
queryset = Criterion.objects.all()
class QuestionListView(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
"""
A View returning every Question Object
"""
serializer_class = QuestionListSerializer
queryset = Question.objects.all()
......@@ -35,6 +35,7 @@ INSTALLED_APPS = [
"corsheaders", # CORS Headers should be as high as possible.
"rest_framework",
# "quiz.apps.QuizConfig",
"modeltranslation", # Needs to be before django.contrib.admin because Admin Panel won't work otherwise
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
......@@ -53,6 +54,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.locale.LocaleMiddleware",
]
# SOURCE: https://github.com/bmihelac/ra-data-django-rest-framework
......@@ -112,7 +114,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGE_CODE = "de"
TIME_ZONE = "UTC"
......@@ -122,6 +124,21 @@ USE_L10N = True
USE_TZ = True
# Internationalization with django-modeltranslation
# https://django-modeltranslation.readthedocs.io/en/latest/index.html
gettext = lambda s: s
LANGUAGES = (
("de", gettext("German")),
("en", gettext("English")),
)
# Modeltranslations Default Language != Djangos Default languages (see LANGUAGE_CODE above)
MODELTRANSLATION_DEFAULT_LANGUAGE = "de"
MODELTRANSLATION_FALLBACK_LANGUAGES = ("de",)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
......
......@@ -21,6 +21,7 @@ from quiz import views
router = routers.DefaultRouter()
router.register(r"sport-list", views.SportListView, "sport-list")
router.register(r"criterion-list", views.CriterionListView, "criterion-list")
router.register(r"question-list", views.QuestionListView, "question-list")
urlpatterns = [
path("admin/", admin.site.urls),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment