Skip to content

Commit

Permalink
Bugfix/deseng575 Language picker should only appear on engagements wi…
Browse files Browse the repository at this point in the history
…th more than one language (#2496)

* DESENG-575 Add language context and call for translation availability

* DESENG-575 Add api route and react context for language detection

* DESENG-575 Add api route and react context for language detection

* DESENG-575 Add unit test for api

* DESENG-575 update changelog

* DESENG-575 fix linter errors

* DESENG-575 fix final lint and test errors

* DESENG-575 Fix linting errors

---------

Co-authored-by: Alex <[email protected]>
  • Loading branch information
Baelx and Alex authored May 10, 2024
1 parent 2ca9a8d commit 0b3a223
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 103 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## May 10, 2024

- **Bugfix** Language picker should only appear on engagements with more than one language [🎟️ DESENG-575](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-575)
- Allow for the web app to see if there are available translations by language
- Disable language switching itself for now until design decisions are made

## May 8, 2024

- **Feature** Add font awesome to MET web [🎟️ DESENG-543](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-543)
Expand Down
14 changes: 11 additions & 3 deletions met-api/src/met_api/models/engagement_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

from __future__ import annotations
from typing import Optional

from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects.postgresql import JSON

from met_api.models.language import Language as LanguageModel
from .base_model import BaseModel
from .db import db

Expand All @@ -17,7 +16,6 @@ class EngagementTranslation(BaseModel):
"""Definition of the Engagement Translation entity."""

__tablename__ = 'engagement_translation'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
engagement_id = db.Column(db.Integer, db.ForeignKey('engagement.id', ondelete='CASCADE'), nullable=False)
language_id = db.Column(db.Integer, db.ForeignKey('language.id', ondelete='CASCADE'), nullable=False)
Expand Down Expand Up @@ -100,3 +98,13 @@ def delete_engagement_translation(engagement_translation_id):
db.session.commit()
return True
return False

@staticmethod
def get_available_translation_languages(engagement_id):
"""Get the list of translations for this engagement, then tally the languages that are used."""
available_translations_query = db.session.query(EngagementTranslation.language_id)\
.filter_by(engagement_id=engagement_id)
language_list = db.session.query(LanguageModel)\
.filter(LanguageModel.id.in_(available_translations_query))\
.all()
return language_list
23 changes: 23 additions & 0 deletions met-api/src/met_api/resources/engagement_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,26 @@ def patch(engagement_id, engagement_translation_id):
return str(err), HTTPStatus.NOT_FOUND
except ValidationError as err:
return str(err.messages), HTTPStatus.BAD_REQUEST


@cors_preflight('GET')
@API.route('/languages')
class EngagementTranslationsLanguages(Resource):
"""Get a list of languages that this engagement is translated into."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(engagement_id):
"""Get a list of all languages available for each of this engagement's translations."""
try:
available_translation_language = (
EngagementTranslationService.get_available_engagement_translation_languages(
engagement_id
)
)
return (
jsonify(available_translation_language),
HTTPStatus.OK,
)
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.BAD_REQUEST
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from met_api.models.engagement_summary_content import EngagementSummary as EngagementSummaryModel
from met_api.models.engagement_translation import EngagementTranslation as EngagementTranslationModel
from met_api.models.language import Language as LanguageModel
from met_api.schemas.language import LanguageSchema
from met_api.schemas.engagement_translation import EngagementTranslationSchema
from met_api.services import authorization
from met_api.utils.roles import Role
Expand Down Expand Up @@ -113,6 +114,14 @@ def delete_engagement_translation(engagement_id, engagement_translation_id):

return EngagementTranslationModel.delete_engagement_translation(engagement_translation_id)

@staticmethod
def get_available_engagement_translation_languages(engagement_id):
"""Get a list of all languages for each entry in the engagement_translation table."""
language_schema = LanguageSchema(many=True)
available_translations = EngagementTranslationModel.get_available_translation_languages(engagement_id)

return language_schema.dump(available_translations)

@staticmethod
def _verify_engagement_translation(engagement_translation_id):
"""Verify if engagement translation exists."""
Expand Down
32 changes: 32 additions & 0 deletions met-api/tests/unit/api/test_engagement_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,35 @@ def test_get_engagement_translation_by_id(client, jwt, session, engagement_trans
rv = client.get(f'/api/engagement/{engagement.id}/translations/{engagement_translation.id}',
headers=headers, content_type=ContentType.JSON.value)
assert rv.status_code == HTTPStatus.INTERNAL_SERVER_ERROR


@pytest.mark.parametrize('engagement_translation_info', [TestEngagementTranslationInfo.engagementtranslation1])
def test_get_available_engagement_translation_languages(client, jwt, session,
engagement_translation_info): # pylint:disable=unused-argument
"""Assert that an engagement with a no translations returns no languages available."""
engagement = factory_engagement_model()
language = factory_language_model({'name': 'French', 'code': 'fr', 'right_to_left': False})

query_url = f'/api/engagement/{engagement.id}/translations/languages'

rv = client.get(query_url, content_type=ContentType.JSON.value)
assert rv.status_code == HTTPStatus.OK
json_data = rv.json
assert json_data == []

engagement_translation_info['engagement_id'] = engagement.id
engagement_translation_info['language_id'] = language.id
factory_engagement_translation_model(engagement_translation_info)

"""Assert that an engagement with a French translation returns
that it has a translation available in the French language."""
rv = client.get(query_url, content_type=ContentType.JSON.value)
assert rv.status_code == HTTPStatus.OK
json_data = rv.json
assert json_data[0]['name'] == 'French'
assert json_data[0]['code'] == 'fr'

with patch.object(EngagementTranslationService, 'get_available_engagement_translation_languages',
side_effect=[KeyError('Test error'), ValueError('Test error')]):
rv = client.get(query_url, content_type=ContentType.JSON.value)
assert rv.status_code == HTTPStatus.BAD_REQUEST
4 changes: 2 additions & 2 deletions met-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { LanguageState } from 'reduxSlices/languageSlice';
import { openNotification } from 'services/notificationService/notificationSlice';
import i18n from './i18n';
import DocumentTitle from 'DocumentTitle';
import { Language } from 'constants/language';
import { Languages } from 'constants/language';
import { AuthKeyCloakContext } from './components/auth/AuthKeycloakContext';
import { determinePathSegments, findTenantInPath } from './utils';

Expand Down Expand Up @@ -117,7 +117,7 @@ const App = () => {
}

try {
const supportedLanguages = Object.values(Language);
const supportedLanguages = Object.values(Languages);
const translationPromises = supportedLanguages.map((languageId) => getTranslationFile(languageId));
const translationFiles = await Promise.all(translationPromises);

Expand Down
3 changes: 3 additions & 0 deletions met-web/src/apiManager/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const Endpoints = {
UPDATE: `${AppConfig.apiUrl}/engagements/`,
GET: `${AppConfig.apiUrl}/engagements/engagement_id`,
},
EngagementTranslations: {
GET_TRANSLATION_LANGUAGES: `${AppConfig.apiUrl}/engagement/engagement_id/translations/languages`,
},
EngagementMetadata: {
GET_BY_ENG: `${AppConfig.apiUrl}/engagements/engagement_id/metadata`,
GET_BY_KEY: `${AppConfig.apiUrl}/engagements/engagement_id/tenant/tenant_id/key`,
Expand Down
38 changes: 38 additions & 0 deletions met-web/src/components/common/LanguageContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { createContext, useState } from 'react';
import { Language } from 'models/language';

export interface LanguageContextType {
engagementViewMounted: boolean;
availableEngagementTranslations: Language[];
setEngagementViewMounted: (engagementViewMounted: boolean) => void;
setAvailableEngagementTranslations: (languages: Language[]) => void;
}

export const LanguageContext = createContext<LanguageContextType>({
engagementViewMounted: false,
availableEngagementTranslations: [],
setEngagementViewMounted: (engagementViewMounted: boolean) => {
/** Left intentionally blank */
},
setAvailableEngagementTranslations: (languages: Language[]) => {
/** Left intentionally blank */
},
});

export const LanguageProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
const [engagementViewMounted, setEngagementViewMounted] = useState(false);
const [availableEngagementTranslations, setAvailableEngagementTranslations] = useState<Language[]>([]);

return (
<LanguageContext.Provider
value={{
engagementViewMounted,
availableEngagementTranslations,
setEngagementViewMounted,
setAvailableEngagementTranslations,
}}
>
{children}
</LanguageContext.Provider>
);
};
129 changes: 67 additions & 62 deletions met-web/src/components/common/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,80 @@
import React, { useMemo, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'hooks';
import { LanguageState, saveLanguage, loadingLanguage } from 'reduxSlices/languageSlice';
import React, { useState, useEffect, useContext } from 'react';
import { useAppDispatch } from 'hooks';
import { MetLabel } from 'components/common';
import { Palette } from 'styles/Theme';
import { Grid, MenuItem, Select } from '@mui/material';
import { useNavigate, useLocation } from 'react-router-dom';
import { Language, LANGUAGE_NAME } from 'constants/language';

interface LanguageDropDownItem {
value: string;
label: string;
}
import { useLocation } from 'react-router-dom';
import { Language } from 'models/language';
import { LanguageContext } from './LanguageContext';

const LanguageSelector = () => {
const dispatch = useAppDispatch();
const language: LanguageState = useAppSelector((state) => state.language);
const navigate = useNavigate();
const location = useLocation();
const { availableEngagementTranslations } = useContext(LanguageContext);
const [languages, setLanguages] = useState<Language[]>([]);

const handleChangeLanguage = (selectedLanguage: string) => {
if (!selectedLanguage) {
dispatch(loadingLanguage(false));
return;
useEffect(() => {
if (availableEngagementTranslations.length > 0) {
// English is assumed to be the "default" language.
const languagesPlusDefault = [
{
id: 0,
name: 'English',
code: 'en',
right_to_left: false,
},
...availableEngagementTranslations,
];
setLanguages(languagesPlusDefault);
} else {
setLanguages([]);
}
}, [availableEngagementTranslations]);

try {
sessionStorage.setItem('languageId', selectedLanguage);
dispatch(
saveLanguage({
id: selectedLanguage,
}),
);

// Change the URL when the language is changed
const pathSegments = location.pathname.split('/');
const languageIndex = pathSegments.findIndex((seg) => seg.length === 2); // Find index of the 2-character language code
if (languageIndex !== -1) {
pathSegments[languageIndex] = selectedLanguage;
} else {
// Language code not found in path, insert it at the appropriate position
pathSegments.splice(1, 0, selectedLanguage);
}
dispatch(loadingLanguage(false));

navigate(pathSegments.join('/'));
} catch {
dispatch(loadingLanguage(false));
console.error('Error occurred while fetching Language information');
}
const handleChangeLanguage = (selectedLanguage: string) => {
// TODO: Implement engagement language switching.
// if (!selectedLanguage) {
// dispatch(loadingLanguage(false));
// return;
// }
// try {
// sessionStorage.setItem('languageId', selectedLanguage);
// dispatch(
// saveLanguage({
// id: selectedLanguage,
// }),
// );
// // Change the URL when the language is changed
// const pathSegments = location.pathname.split('/');
// const languageIndex = pathSegments.findIndex((seg) => seg.length === 2); // Find index of the 2-character language code
// if (languageIndex !== -1) {
// pathSegments[languageIndex] = selectedLanguage;
// } else {
// // Language code not found in path, insert it at the appropriate position
// pathSegments.splice(1, 0, selectedLanguage);
// }
// dispatch(loadingLanguage(false));
// navigate(pathSegments.join('/'));
// } catch {
// dispatch(loadingLanguage(false));
// console.error('Error occurred while fetching Language information');
// }
};

const ITEMS: LanguageDropDownItem[] = useMemo(() => {
return Object.values(Language).map((lang) => ({
value: lang,
label: LANGUAGE_NAME[lang],
}));
}, [LANGUAGE_NAME]);

useEffect(() => {
// TODO: Add logic to update URL when engagement language changes.
// Update language dropdown when the language ID in the URL changes
const pathSegments = location.pathname.split('/');
const languageIndex = pathSegments.findIndex((seg) => seg.length === 2); // Find index of the 2-character language code
if (languageIndex !== -1) {
const languageId = pathSegments[languageIndex].toLowerCase();
sessionStorage.setItem('languageId', languageId);
dispatch(
saveLanguage({
id: languageId,
}),
);
}
// const pathSegments = location.pathname.split('/');
// const languageIndex = pathSegments.findIndex((seg) => seg.length === 2); // Find index of the 2-character language code
// if (languageIndex !== -1) {
// const languageId = pathSegments[languageIndex].toLowerCase();
// sessionStorage.setItem('languageId', languageId);
// dispatch(
// saveLanguage({
// id: languageId,
// }),
// );
// }
}, [dispatch, location.pathname]);

return (
Expand All @@ -81,7 +86,7 @@ const LanguageSelector = () => {
<Select
id="language"
aria-label="select-language"
value={language.id}
value={0}
size="small"
sx={{
backgroundColor: 'var(--bcds-surface-background-white)',
Expand All @@ -90,9 +95,9 @@ const LanguageSelector = () => {
}}
onChange={(event) => handleChangeLanguage(event.target.value as string)}
>
{ITEMS.map((item) => (
<MenuItem key={item.value} value={item.value}>
{item.label}
{languages.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.name}
</MenuItem>
))}
</Select>
Expand Down
Loading

0 comments on commit 0b3a223

Please sign in to comment.