Skip to content

Commit

Permalink
Added initial unit testing
Browse files Browse the repository at this point in the history
  • Loading branch information
wes-otf committed Oct 18, 2024
1 parent de5d822 commit fc7608a
Show file tree
Hide file tree
Showing 8 changed files with 415 additions and 37 deletions.
2 changes: 1 addition & 1 deletion hypha/apply/funds/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from wagtail.signal_handlers import disable_reference_index_auto_update

from hypha.apply.categories.models import MetaTerm
from hypha.apply.translate.translate import get_available_translations
from hypha.apply.translate.utils import get_available_translations
from hypha.apply.users.models import User

from .fields import LanguageChoiceField
Expand Down
193 changes: 192 additions & 1 deletion hypha/apply/funds/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import json
from unittest.mock import Mock, patch
from urllib.parse import parse_qs, urlparse

import pytest
from django.test import TestCase, override_settings
from freezegun import freeze_time

from hypha.apply.funds.utils import get_copied_form_name
from hypha.apply.funds.utils import get_copied_form_name, get_language_choices_json

date = "2024-10-16 15:05:13.721861"

Expand All @@ -22,3 +27,189 @@
@pytest.mark.parametrize("original_name,expected", form_name_dataset)
def test_get_copied_form_name(original_name, expected):
assert get_copied_form_name(original_name) == expected


# Setup for testing `get_language_choices_json`


def equal_ignore_order(a: list, b: list) -> bool:
"""Used to compare two lists that are unsortable & unhashable
Primarily used when comparing the result of json.loads
Args:
a: the first unhashable & unsortable list
b: the second unhashable & unsortable list
Returns:
bool: true when lists are equal in length & content
"""
if len(a) != len(b):
return False
unmatched = list(b)
for element in a:
try:
unmatched.remove(element)
except ValueError:
return False
return not unmatched


class TestGetLanguageChoices(TestCase):
@staticmethod
def mocked_get_lang_name(code):
# Added "!" to ensure the mock is being called rather than the actual get_lang
codes_to_lang = {
"en": "English!",
"ar": "Arabic!",
"zh": "Chinese!",
"fr": "French!",
}
return codes_to_lang[code]

@staticmethod
def mocked_get_translation_params(url):
query_dict = {k: v[0] for (k, v) in parse_qs(urlparse(url).query).items()}
if (to_lang := query_dict.get("tl")) and (from_lang := query_dict.get("fl")):
return (from_lang, to_lang)

@classmethod
def setUpClass(cls):
"""Used to patch & mock all the methods called from hypha.apply.translate.utils"""
available_packages = [
Mock(from_code="ar", to_code="en"),
Mock(from_code="fr", to_code="en"),
Mock(from_code="en", to_code="ar"),
Mock(from_code="zh", to_code="en"),
Mock(from_code="en", to_code="fr"),
]

cls.patcher = [
patch(
"hypha.apply.translate.utils.get_lang_name",
side_effect=cls.mocked_get_lang_name,
),
patch(
"hypha.apply.translate.utils.get_available_translations",
return_value=available_packages,
),
patch(
"hypha.apply.translate.utils.get_translation_params",
side_effect=cls.mocked_get_translation_params,
),
]

for patched in cls.patcher:
patched.start()

@classmethod
def tearDownClass(cls):
for patched in cls.patcher:
patched.stop()

def test_get_language_choices_json(self):
expected_json = [
{
"label": "English!",
"selected": False,
"to": [
{"label": "Arabic!", "selected": True, "value": "ar"},
{"label": "French!", "selected": False, "value": "fr"},
],
"value": "en",
},
{
"label": "Arabic!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "ar",
},
{
"label": "Chinese!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "zh",
},
{
"label": "French!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "fr",
},
]
request = Mock(headers={})

json_out = get_language_choices_json(request)
self.assertTrue(equal_ignore_order(json.loads(json_out), expected_json))

def test_get_language_choices_json_with_current_url(self):
expected_json = [
{
"label": "English!",
"selected": False,
"to": [
{"label": "Arabic!", "selected": True, "value": "ar"},
{"label": "French!", "selected": False, "value": "fr"},
],
"value": "en",
},
{
"label": "Arabic!",
"selected": True,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "ar",
},
{
"label": "Chinese!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "zh",
},
{
"label": "French!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "fr",
},
]
current_url = "https://hyphaiscool.org/apply/submissions/6/?fl=ar&tl=en"
request = Mock(headers={"Hx-Current-Url": current_url})

json_out = get_language_choices_json(request)
self.assertTrue(equal_ignore_order(json.loads(json_out), expected_json))

@override_settings(LANGUAGE_CODE="fr")
def test_get_language_choices_json_with_language_code(self):
expected_json = [
{
"label": "English!",
"selected": False,
"to": [
{"label": "Arabic!", "selected": False, "value": "ar"},
{"label": "French!", "selected": True, "value": "fr"},
],
"value": "en",
},
{
"label": "Arabic!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "ar",
},
{
"label": "Chinese!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "zh",
},
{
"label": "French!",
"selected": False,
"to": [{"label": "English!", "selected": True, "value": "en"}],
"value": "fr",
},
]
request = Mock(headers={})

json_out = get_language_choices_json(request)
self.assertTrue(equal_ignore_order(json.loads(json_out), expected_json))
21 changes: 12 additions & 9 deletions hypha/apply/funds/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
from django.utils.html import strip_tags
from django.utils.translation import gettext as _

from hypha.apply.translate.translate import get_available_translations
from hypha.apply.translate.utils import get_lang_name, get_translation_params
from hypha.apply.translate import utils as translate_utils
from hypha.apply.utils.image import generate_image_tag

from .models.screening import ScreeningStatus
Expand Down Expand Up @@ -198,7 +197,7 @@ def get_copied_form_name(original_form_name: str) -> str:
return f"{new_name} ({copy_str.format(copy_time=copy_time)})"


def get_language_choices_json(request: HttpRequest = None) -> str:
def get_language_choices_json(request: HttpRequest) -> str:
"""Generate a JSON output of available translation options
Utilized for populating the reactive form fields on the client side
Expand Down Expand Up @@ -227,15 +226,15 @@ def get_language_choices_json(request: HttpRequest = None) -> str:
]
```
"""
available_translations = get_available_translations()
available_translations = translate_utils.get_available_translations()
from_langs = {package.from_code for package in available_translations}
default_to_lang = settings.LANGUAGE_CODE
default_to_lang = settings.LANGUAGE_CODE if settings.LANGUAGE_CODE else None
default_from_lang = None

# If there's existing lang params, use those as the default in the form
# ie. the user has an active translation for ar -> en, those should be selected in the form
if (current_url := request.headers.get("Hx-Current-Url")) and (
params := get_translation_params(current_url)
params := translate_utils.get_translation_params(current_url)
):
default_from_lang, default_to_lang = params

Expand All @@ -249,12 +248,16 @@ def get_language_choices_json(request: HttpRequest = None) -> str:

# Set the default selection to be the default_to_lang if it exists in the to_langs list,
# otherwise use the first value in the list.
selected_to = default_to_lang if default_to_lang in to_langs else to_langs[0]
selected_to = (
default_to_lang
if default_to_lang and default_to_lang in to_langs
else to_langs[0]
)

to_choices = [
{
"value": to_lang,
"label": get_lang_name(to_lang),
"label": translate_utils.get_lang_name(to_lang),
"selected": to_lang == selected_to,
}
for to_lang in to_langs
Expand All @@ -263,7 +266,7 @@ def get_language_choices_json(request: HttpRequest = None) -> str:
choices.append(
{
"value": lang,
"label": get_lang_name(lang),
"label": translate_utils.get_lang_name(lang),
"to": to_choices,
"selected": lang == default_from_lang,
}
Expand Down
Empty file.
71 changes: 71 additions & 0 deletions hypha/apply/translate/tests/test_translate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import List
from unittest import TestCase
from unittest.mock import Mock, patch

from hypha.apply.translate.translate import translate


class TestTranslate(TestCase):
@staticmethod
def mocked_translate(string: str, from_code, to_code):
"""Use pig latin for all test translations - ie. 'hypha is cool' -> 'yphahay isway oolcay'
https://en.wikipedia.org/wiki/Pig_Latin
"""
vowels = {"a", "e", "i", "o", "u"}
string = string.lower()
pl = [
f"{word}way" if word[0] in vowels else f"{word[1:]}{word[0]}ay"
for word in string.split()
]
return " ".join(pl)

@staticmethod
def mocked_get_available_translations(codes: List[str]):
mocked_packages = [
Mock(from_code="ar", to_code="en"),
Mock(from_code="fr", to_code="en"),
Mock(from_code="en", to_code="ar"),
Mock(from_code="zh", to_code="en"),
Mock(from_code="en", to_code="fr"),
]

return list(filter(lambda x: x.from_code in codes, mocked_packages))

@classmethod
def setUpClass(cls):
"""Used to patch & mock all the methods called from argostranslate & hypha.apply.translate.utils"""

cls.patcher = [
patch(
"hypha.apply.translate.utils.get_available_translations",
side_effect=cls.mocked_get_available_translations,
),
patch(
"argostranslate.translate.translate", side_effect=cls.mocked_translate
),
]

for patched in cls.patcher:
patched.start()

@classmethod
def tearDownClass(cls):
for patched in cls.patcher:
patched.stop()

def test_valid_translate(self):
self.assertEqual(translate("hey there", "fr", "en"), "eyhay heretay")

def test_duplicate_code_translate(self):
with self.assertRaises(ValueError) as context:
translate("hey there", "fr", "fr")

self.assertEqual(
"Translation from_code cannot match to_code", str(context.exception)
)

def test_invalid_code_translate(self):
with self.assertRaises(ValueError) as context:
translate("hey there", "test", "test2")

self.assertIn("is not installed", str(context.exception))
Loading

0 comments on commit fc7608a

Please sign in to comment.