diff --git a/core/controllers/community_dashboard.py b/core/controllers/community_dashboard.py index 1a127fe9bd4b..e9d617aba76e 100644 --- a/core/controllers/community_dashboard.py +++ b/core/controllers/community_dashboard.py @@ -20,6 +20,7 @@ from constants import constants from core.controllers import acl_decorators from core.controllers import base +from core.domain import config_domain from core.domain import exp_fetchers from core.domain import opportunity_services from core.domain import topic_fetchers @@ -233,3 +234,17 @@ def get(self): community_rights.can_review_questions if community_rights else False) }) + + +class FeaturedTranslationLanguagesHandler(base.BaseHandler): + """Provides featured translation languages set in admin config.""" + + GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON + + @acl_decorators.open_access + def get(self): + """Handles GET requests.""" + self.render_json({ + 'featured_translation_languages': + config_domain.FEATURED_TRANSLATION_LANGUAGES.value + }) diff --git a/core/controllers/community_dashboard_test.py b/core/controllers/community_dashboard_test.py index becaa6edaf79..1e97b044acc8 100644 --- a/core/controllers/community_dashboard_test.py +++ b/core/controllers/community_dashboard_test.py @@ -17,6 +17,7 @@ from __future__ import absolute_import # pylint: disable=import-only-modules from __future__ import unicode_literals # pylint: disable=import-only-modules +from core.domain import config_services from core.domain import exp_domain from core.domain import exp_services from core.domain import story_domain @@ -452,3 +453,29 @@ def test_user_check_community_rights(self): 'can_review_voiceover_for_language_codes': [], 'can_review_questions': True }) + + +class FeaturedTranslationLanguagesHandlerTest(test_utils.GenericTestBase): + """Test for the FeaturedTranslationLanguagesHandler.""" + + def test_get_featured_translation_languages(self): + response = self.get_json('/retrivefeaturedtranslationlanguages') + self.assertEqual( + response, + {'featured_translation_languages': []} + ) + + new_value = [ + {'language_code': 'en', 'explanation': 'Partnership with ABC'} + ] + config_services.set_property( + 'admin', + 'featured_translation_languages', + new_value + ) + + response = self.get_json('/retrivefeaturedtranslationlanguages') + self.assertEqual( + response, + {'featured_translation_languages': new_value} + ) diff --git a/core/domain/config_domain.py b/core/domain/config_domain.py index e3ea2d3dea33..a59f80375814 100644 --- a/core/domain/config_domain.py +++ b/core/domain/config_domain.py @@ -30,6 +30,27 @@ CMD_CHANGE_PROPERTY_VALUE = 'change_property_value' +LIST_OF_FEATURED_TRANSLATION_LANGUAGES_DICTS_SCHEMA = { + 'type': 'list', + 'items': { + 'type': 'dict', + 'properties': [{ + 'name': 'language_code', + 'schema': { + 'type': 'unicode', + 'validators': [{ + 'id': 'is_supported_audio_language_code', + }] + }, + }, { + 'name': 'explanation', + 'schema': { + 'type': 'unicode' + } + }] + } +} + SET_OF_STRINGS_SCHEMA = { 'type': 'list', 'items': { @@ -345,3 +366,9 @@ def get_all_config_property_names(cls): CLASSROOM_PAGE_IS_SHOWN = ConfigProperty( 'classroom_page_is_shown', BOOL_SCHEMA, 'Show classroom components.', False) + +FEATURED_TRANSLATION_LANGUAGES = ConfigProperty( + 'featured_translation_languages', + LIST_OF_FEATURED_TRANSLATION_LANGUAGES_DICTS_SCHEMA, + 'Featured Translation Languages', [] +) diff --git a/core/templates/components/oppia-angular-root.component.ts b/core/templates/components/oppia-angular-root.component.ts index 8caa102b757a..de6f74d23d40 100644 --- a/core/templates/components/oppia-angular-root.component.ts +++ b/core/templates/components/oppia-angular-root.component.ts @@ -233,6 +233,8 @@ import { ExtensionTagAssemblerService } from import { ExtractImageFilenamesFromStateService } from // eslint-disable-next-line max-len 'pages/exploration-player-page/services/extract-image-filenames-from-state.service'; +import { FeaturedTranslationLanguageObjectFactory } from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; import { FeedbackMessageSummaryObjectFactory } from 'domain/feedback_message/FeedbackMessageSummaryObjectFactory'; import { FeedbackThreadObjectFactory } from @@ -706,6 +708,7 @@ export class OppiaAngularRootComponent implements AfterViewInit { static expressionSyntaxTreeService: ExpressionSyntaxTreeService; static extensionTagAssemblerService: ExtensionTagAssemblerService; static extractImageFilenamesFromStateService: ExtractImageFilenamesFromStateService; + static featuredTranslationLanguageObjectFactory: FeaturedTranslationLanguageObjectFactory; static feedbackMessageSummaryObjectFactory: FeedbackMessageSummaryObjectFactory; static feedbackThreadObjectFactory: FeedbackThreadObjectFactory; static feedbackThreadSummaryObjectFactory: FeedbackThreadSummaryObjectFactory; @@ -989,6 +992,7 @@ private explorationTaskObjectFactory: ExplorationTaskObjectFactory, private expressionSyntaxTreeService: ExpressionSyntaxTreeService, private extensionTagAssemblerService: ExtensionTagAssemblerService, private extractImageFilenamesFromStateService: ExtractImageFilenamesFromStateService, +private featuredTranslationLanguageObjectFactory: FeaturedTranslationLanguageObjectFactory, private feedbackMessageSummaryObjectFactory: FeedbackMessageSummaryObjectFactory, private feedbackThreadObjectFactory: FeedbackThreadObjectFactory, private feedbackThreadSummaryObjectFactory: FeedbackThreadSummaryObjectFactory, @@ -1273,6 +1277,7 @@ private writtenTranslationsObjectFactory: WrittenTranslationsObjectFactory OppiaAngularRootComponent.expressionSyntaxTreeService = this.expressionSyntaxTreeService; OppiaAngularRootComponent.extensionTagAssemblerService = this.extensionTagAssemblerService; OppiaAngularRootComponent.extractImageFilenamesFromStateService = this.extractImageFilenamesFromStateService; + OppiaAngularRootComponent.featuredTranslationLanguageObjectFactory = this.featuredTranslationLanguageObjectFactory; OppiaAngularRootComponent.feedbackMessageSummaryObjectFactory = this.feedbackMessageSummaryObjectFactory; OppiaAngularRootComponent.feedbackThreadObjectFactory = this.feedbackThreadObjectFactory; OppiaAngularRootComponent.feedbackThreadSummaryObjectFactory = this.feedbackThreadSummaryObjectFactory; diff --git a/core/templates/domain/opportunity/FeaturedTranslationLanguageObjectFactory.ts b/core/templates/domain/opportunity/FeaturedTranslationLanguageObjectFactory.ts new file mode 100644 index 000000000000..a2552730d6ec --- /dev/null +++ b/core/templates/domain/opportunity/FeaturedTranslationLanguageObjectFactory.ts @@ -0,0 +1,51 @@ +// Copyright 2020 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Factory for creating and mutating instances of frontend + * featured translation language domain objects. + */ + +import { downgradeInjectable } from '@angular/upgrade/static'; +import { Injectable } from '@angular/core'; + +export interface IFeaturedTranslationLanguageBackendDict { + 'language_code': string; + explanation: string; +} + +export class FeaturedTranslationLanguage { + constructor( + readonly languageCode: string, + readonly explanation: string + ) {} +} + +@Injectable({ + providedIn: 'root' +}) +export class FeaturedTranslationLanguageObjectFactory { + createFromBackendDict( + featuredTranslationBackendDict: IFeaturedTranslationLanguageBackendDict + ): FeaturedTranslationLanguage { + return new FeaturedTranslationLanguage( + featuredTranslationBackendDict.language_code, + featuredTranslationBackendDict.explanation + ); + } +} + +angular.module('oppia').factory( + 'FeaturedTranslationLanguageObjectFactory', + downgradeInjectable(FeaturedTranslationLanguageObjectFactory)); diff --git a/core/templates/domain/opportunity/featured-translation-language-object.factory.spec.ts b/core/templates/domain/opportunity/featured-translation-language-object.factory.spec.ts new file mode 100644 index 000000000000..7f6a6d623490 --- /dev/null +++ b/core/templates/domain/opportunity/featured-translation-language-object.factory.spec.ts @@ -0,0 +1,50 @@ +// Copyright 2020 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Tests for FeaturedTranslationLanguageObjectFactory. + */ + +import { TestBed } from '@angular/core/testing'; + +import { + FeaturedTranslationLanguageObjectFactory, + FeaturedTranslationLanguage, + IFeaturedTranslationLanguageBackendDict +} from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; + +describe('Featured Translation Language object factory', () => { + let featuredTranslationLanguageObjectFactory: + FeaturedTranslationLanguageObjectFactory = null; + let sampleFTL: FeaturedTranslationLanguage = null; + + beforeEach(() => { + featuredTranslationLanguageObjectFactory = TestBed.get( + FeaturedTranslationLanguageObjectFactory); + + let sampleFTLDict: IFeaturedTranslationLanguageBackendDict = { + language_code: 'en', + explanation: 'English' + }; + sampleFTL = featuredTranslationLanguageObjectFactory + .createFromBackendDict(sampleFTLDict); + }); + + it('should correctly evaluate all the values based on backend' + + ' dict', function() { + expect(sampleFTL.languageCode).toBe('en'); + expect(sampleFTL.explanation).toBe('English'); + }); +}); diff --git a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.html b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.html index cd7a039bab7e..d350107dc0a8 100644 --- a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.html +++ b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.html @@ -54,12 +54,10 @@ <[$ctrl.tabsDetails[$ctrl.activeTabName].description]>
- Translate text to: - + Translate to + +
@@ -80,13 +78,13 @@ .oppia-dashboard-language-container-label { color: #4a4a4a; font-size: 18px; + margin-top: 10px; } .oppia-dashboard-language-container { display: flex; flex-direction: column; - height: 80px; margin-left: 10%; - width: 15%; + width: 250px; } .oppia-opportunity-language-selector { background: white; @@ -94,9 +92,8 @@ height: 35px; } .oppia-opportunities-tabs-explanation { - padding-top: 25px; + padding-top: 50px; position: relative; - width: 40%; } .oppia-opportunities-tabs-explanation::before { bottom: 65%; diff --git a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.spec.ts b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.spec.ts index 623b1c46c8aa..8f04dcb71c27 100644 --- a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.spec.ts +++ b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.spec.ts @@ -100,16 +100,6 @@ describe('Community dashboard page', function() { expect(ctrl.profilePictureDataUrl).toBe(userProfileImage); }); - it('should get all language codes and its descriptions', function() { - const allLanguageCodesAndDescriptionsFromConstants = ( - CONSTANTS.SUPPORTED_AUDIO_LANGUAGES.map(language => ({ - id: language.id, - description: language.description - }))); - expect(ctrl.languageCodesAndDescriptions).toEqual( - allLanguageCodesAndDescriptionsFromConstants); - }); - it('should change active tab name', function() { var changedTab = 'translateTextTab'; expect(ctrl.activeTabName).toBe('myContributionTab'); @@ -121,7 +111,7 @@ describe('Community dashboard page', function() { spyOn(LocalStorageService, 'updateLastSelectedTranslationLanguageCode') .and.callThrough(); - ctrl.onChangeLanguage(); + ctrl.onChangeLanguage('hi'); expect(TranslationLanguageService.setActiveLanguageCode) .toHaveBeenCalledWith('hi'); diff --git a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.ts b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.ts index 4ccd2a67c0ef..a1aa0d48a3ee 100644 --- a/core/templates/pages/community-dashboard-page/community-dashboard-page.component.ts +++ b/core/templates/pages/community-dashboard-page/community-dashboard-page.component.ts @@ -26,6 +26,9 @@ require( require( 'pages/community-dashboard-page/contributions-and-review/' + 'contributions-and-review.directive.ts'); +require( + 'pages/community-dashboard-page/translation-language-selector/' + + 'translation-language-selector.component.ts'); require( 'pages/community-dashboard-page/question-opportunities/' + 'question-opportunities.directive.ts'); @@ -73,7 +76,8 @@ angular.module('oppia').component('communityDashboardPage', { return languageDescriptions; }; - ctrl.onChangeLanguage = function() { + ctrl.onChangeLanguage = function(languageCode: string) { + ctrl.languageCode = languageCode; TranslationLanguageService.setActiveLanguageCode(ctrl.languageCode); LocalStorageService.updateLastSelectedTranslationLanguageCode( ctrl.languageCode); @@ -135,15 +139,6 @@ angular.module('oppia').component('communityDashboardPage', { } }); - ctrl.languageCodesAndDescriptions = ( - allAudioLanguageCodes.map(function(languageCode) { - return { - id: languageCode, - description: ( - LanguageUtilService.getAudioLanguageDescription( - languageCode)) - }; - })); ctrl.languageCode = ( allAudioLanguageCodes.indexOf(prevSelectedLanguageCode) !== -1 ? prevSelectedLanguageCode : DEFAULT_OPPORTUNITY_LANGUAGE_CODE); diff --git a/core/templates/pages/community-dashboard-page/community-dashboard-page.module.ts b/core/templates/pages/community-dashboard-page/community-dashboard-page.module.ts index 9f5b7ad74c96..29116352864a 100644 --- a/core/templates/pages/community-dashboard-page/community-dashboard-page.module.ts +++ b/core/templates/pages/community-dashboard-page/community-dashboard-page.module.ts @@ -26,7 +26,7 @@ angular.module('oppia', [ 'toastr', 'ui.bootstrap', 'ui.sortable', 'ui.tree', 'ui.validate' ]); -import { Component, NgModule, StaticProvider } from '@angular/core'; +import { NgModule, StaticProvider } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { downgradeComponent } from '@angular/upgrade/static'; import { HttpClientModule } from '@angular/common/http'; @@ -39,6 +39,8 @@ import { OppiaAngularRootComponent } from import { AppConstants } from 'app.constants'; import { CommunityDashboardConstants } from 'pages/community-dashboard-page/community-dashboard-page.constants'; +import { TranslationLanguageSelectorComponent } from + './translation-language-selector/translation-language-selector.component'; @NgModule({ imports: [ @@ -47,10 +49,12 @@ import { CommunityDashboardConstants } from SharedComponentsModule ], declarations: [ - OppiaAngularRootComponent + OppiaAngularRootComponent, + TranslationLanguageSelectorComponent ], entryComponents: [ - OppiaAngularRootComponent + OppiaAngularRootComponent, + TranslationLanguageSelectorComponent ], providers: [ AppConstants, diff --git a/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts b/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts index 33c7867d8153..33d4a1a8b5ed 100644 --- a/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts +++ b/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.spec.ts @@ -29,6 +29,8 @@ import { SkillOpportunityObjectFactory } from 'domain/opportunity/SkillOpportunityObjectFactory'; import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import { FeaturedTranslationLanguageObjectFactory} from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; describe('Contribution Opportunities backend API service', function() { let contributionOpportunitiesBackendApiService: @@ -176,4 +178,36 @@ describe('Contribution Opportunities backend API service', function() { expect(failHandler).not.toHaveBeenCalled(); }) ); + + it('should successfully fetch the featured translation languages', + fakeAsync(() => { + const successHandler = jasmine.createSpy('success'); + const failHandler = jasmine.createSpy('fail'); + + const featuredTranslationLanguageObjectFactory = TestBed.get( + FeaturedTranslationLanguageObjectFactory); + + contributionOpportunitiesBackendApiService + .fetchFeaturedTranslationLanguages() + .then(successHandler, failHandler); + + const req = httpTestingController.expectOne( + '/retrivefeaturedtranslationlanguages' + ); + expect(req.request.method).toEqual('GET'); + req.flush({ + featured_translation_languages: + [{ language_code: 'en', explanation: 'English' }] + }); + + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalledWith([ + featuredTranslationLanguageObjectFactory.createFromBackendDict( + { language_code: 'en', explanation: 'English' } + ) + ]); + expect(failHandler).not.toHaveBeenCalled(); + }) + ); }); diff --git a/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.ts b/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.ts index 1270525cdbc0..dab7ee2533f6 100644 --- a/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.ts +++ b/core/templates/pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.ts @@ -27,6 +27,10 @@ import { SkillOpportunity } from 'domain/opportunity/SkillOpportunityObjectFactory'; import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; +import { + FeaturedTranslationLanguageObjectFactory, + IFeaturedTranslationLanguageBackendDict, +} from 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; const constants = require('constants.ts'); @@ -45,7 +49,9 @@ export class ContributionOpportunitiesBackendApiService { urlTemplate = '/opportunitiessummaryhandler/'; constructor( private urlInterpolationService: UrlInterpolationService, - private http: HttpClient + private http: HttpClient, + private featuredTranslationLanguageObjectFactory: + FeaturedTranslationLanguageObjectFactory ) {} // TODO(#7165): Replace any with exact type. @@ -74,7 +80,7 @@ export class ContributionOpportunitiesBackendApiService { successCallback: ( opportunities?: Array, nextCursor?: string, more?: boolean ) => void, - errorCallback: (reason?: any) => void + errorCallback: (reason: string) => void ): void { this.http.get(this.urlInterpolationService.interpolateUrl( this.urlTemplate, { opportunityType } @@ -129,6 +135,25 @@ export class ContributionOpportunitiesBackendApiService { params, resolve, reject); }); } + + async fetchFeaturedTranslationLanguages(): Promise { + try { + const response = await this.http + .get('/retrivefeaturedtranslationlanguages') + .toPromise() as { + 'featured_translation_languages': + IFeaturedTranslationLanguageBackendDict[] + }; + + return response.featured_translation_languages.map( + (backendDict: IFeaturedTranslationLanguageBackendDict) => + this.featuredTranslationLanguageObjectFactory + .createFromBackendDict(backendDict) + ); + } catch { + return []; + } + } } angular.module('oppia').factory( diff --git a/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.html b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.html new file mode 100644 index 000000000000..139bac627a3d --- /dev/null +++ b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.html @@ -0,0 +1,135 @@ +
+ +
+ {{languageIdToDescription[activeLanguageCode]}} + + arrow_drop_down + +
+ +
+ + +
All languages
+
+ {{option.description}} +
+
+
+ + diff --git a/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts new file mode 100644 index 000000000000..65f2d8d53ebf --- /dev/null +++ b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.spec.ts @@ -0,0 +1,160 @@ +// Copyright 2020 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Unit tests for the translation language selector component. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TranslationLanguageSelectorComponent } from + // eslint-disable-next-line max-len + 'pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.ts'; +import { ContributionOpportunitiesBackendApiService } from + // eslint-disable-next-line max-len + 'pages/community-dashboard-page/services/contribution-opportunities-backend-api.service.ts'; +import { FeaturedTranslationLanguageObjectFactory } from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; + +describe('Translation language selector', () => { + let component: TranslationLanguageSelectorComponent; + let fixture: ComponentFixture; + + let featuredTranslationLanguageObjectFactory = ( + new FeaturedTranslationLanguageObjectFactory()); + let featuredLanguages = [ + featuredTranslationLanguageObjectFactory.createFromBackendDict({ + language_code: 'fr', + explanation: 'Partnership with ABC' + }), + featuredTranslationLanguageObjectFactory.createFromBackendDict({ + language_code: 'de', + explanation: 'Partnership with CBA' + }) + ]; + + let contributionOpportunitiesBackendApiServiceStub: + Partial = { + fetchFeaturedTranslationLanguages: () => + Promise.resolve(featuredLanguages) + }; + + let clickDropdown: () => void; + let getDropdownOptionsContainer: () => HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TranslationLanguageSelectorComponent], + providers: [{ + provide: ContributionOpportunitiesBackendApiService, + useValue: contributionOpportunitiesBackendApiServiceStub + }] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TranslationLanguageSelectorComponent); + component = fixture.componentInstance; + component.activeLanguageCode = 'en'; + fixture.detectChanges(); + }); + + beforeEach(() => { + clickDropdown = () => { + fixture.debugElement.nativeElement + .querySelector('.oppia-translation-language-selector-inner-container') + .click(); + fixture.detectChanges(); + }; + + getDropdownOptionsContainer = () => { + return fixture.debugElement.nativeElement.querySelector( + '.oppia-translation-language-selector-dropdown-container'); + }; + }); + + it('should correctly initialize languageIdToDescription map', () => { + expect(component.languageIdToDescription.en).toBe('English'); + expect(component.languageIdToDescription.fr).toBe('French'); + }); + + it('should correctly fetch featured languages', async(() => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(component.featuredLanguages).toEqual(featuredLanguages); + }); + })); + + it('should correctly initialize dropdown activeLanguageCode', () => { + const dropdown = (fixture.nativeElement + .querySelector('.oppia-translation-language-selector-inner-container')); + + expect(dropdown.firstChild.textContent.trim()).toBe('English'); + }); + + it('should correctly show and hide the dropdown', () => { + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + + clickDropdown(); + expect(component.dropdownShown).toBe(true); + expect(getDropdownOptionsContainer()).toBeTruthy(); + + clickDropdown(); + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + + clickDropdown(); + expect(component.dropdownShown).toBe(true); + expect(getDropdownOptionsContainer()).toBeTruthy(); + + let fakeClickAwayEvent = new MouseEvent('click'); + Object.defineProperty( + fakeClickAwayEvent, + 'target', + {value: document.createElement('div')}); + component.onDocumentClick(fakeClickAwayEvent); + fixture.detectChanges(); + expect(component.dropdownShown).toBe(false); + expect(getDropdownOptionsContainer()).toBeFalsy(); + }); + + it('should correctly select and indicate selection of an option', () => { + spyOn(component.setActiveLanguageCode, 'emit'); + + component.selectOption('fr'); + fixture.detectChanges(); + + expect(component.setActiveLanguageCode.emit).toHaveBeenCalledWith('fr'); + }); + + it('should show details of featured language', async(() => { + clickDropdown(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + component.showExplanationPopup(0); + fixture.detectChanges(); + + expect(component.explanationPopupContent) + .toBe('Partnership with ABC'); + expect(component.explanationPopupShown).toBe(true); + + component.hideExplanationPopup(); + fixture.detectChanges(); + expect(component.explanationPopupShown).toBe(false); + }); + })); +}); diff --git a/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.ts b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.ts new file mode 100644 index 000000000000..0521ea4c16e1 --- /dev/null +++ b/core/templates/pages/community-dashboard-page/translation-language-selector/translation-language-selector.component.ts @@ -0,0 +1,118 @@ +// Copyright 2020 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Component for the translation language select. + */ + +import { + Component, OnInit, Input, Output, EventEmitter, HostListener, ViewChild +} from '@angular/core'; +import { downgradeComponent } from '@angular/upgrade/static'; + +import { ContributionOpportunitiesBackendApiService } from + // eslint-disable-next-line max-len + 'pages/community-dashboard-page/services/contribution-opportunities-backend-api.service'; +import { FeaturedTranslationLanguage } from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; +import { LanguageUtilService } from 'domain/utilities/language-util.service'; + +@Component({ + selector: 'translation-language-selector', + templateUrl: './translation-language-selector.component.html' +}) +export class TranslationLanguageSelectorComponent implements OnInit { + @Input() activeLanguageCode: string; + @Output() setActiveLanguageCode: EventEmitter = new EventEmitter(); + @ViewChild('dropdown', {'static': false}) dropdownRef; + + options: {id: string, description: string}[]; + languageIdToDescription: {[id: string]: string} = {}; + featuredLanguages: FeaturedTranslationLanguage[] = []; + + dropdownShown = false; + explanationPopupShown = false; + explanationPopupPxOffsetY = 0; + explanationPopupContent = ''; + + constructor( + private contributionOpportunitiesBackendApiService: + ContributionOpportunitiesBackendApiService, + private languageUtilService: LanguageUtilService + ) {} + + ngOnInit() { + this.options = this.languageUtilService + .getAllVoiceoverLanguageCodes().map(languageCode => { + const description = this.languageUtilService + .getAudioLanguageDescription(languageCode); + this.languageIdToDescription[languageCode] = description; + return { id: languageCode, description }; + }); + + this.contributionOpportunitiesBackendApiService + .fetchFeaturedTranslationLanguages() + .then((featuredLanguages: FeaturedTranslationLanguage[]) => { + this.featuredLanguages = featuredLanguages; + }); + } + + toggleDropdown() { + this.dropdownShown = !this.dropdownShown; + } + + selectOption(activeLanguageCode: string) { + this.setActiveLanguageCode.emit(activeLanguageCode); + this.dropdownShown = false; + } + + showExplanationPopup(index: number) { + /** + * Align popup to mouse-overed info icon. + * 75: approximate height of selector and featured languages label. + * 30: approximate height of each dropdown element. + */ + this.explanationPopupPxOffsetY = 75 + 30 * index; + this.explanationPopupContent = ( + this.featuredLanguages[index].explanation); + this.explanationPopupShown = true; + } + + hideExplanationPopup() { + this.explanationPopupShown = false; + } + + /** + * Close dropdown when outside elements are clicked + * @param event mouse click event + */ + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + const targetElement = event.target as HTMLElement; + if ( + targetElement && + !this.dropdownRef.nativeElement.contains(targetElement) + ) { + this.dropdownShown = false; + } + } +} + +angular.module('oppia').directive( + 'translationLanguageSelector', + downgradeComponent({ + component: TranslationLanguageSelectorComponent, + inputs: ['activeLanguageCode'], + outputs: ['setActiveLanguageCode'] + })); diff --git a/core/templates/services/UpgradedServices.ts b/core/templates/services/UpgradedServices.ts index 966d7a8f07fe..204032351e5e 100644 --- a/core/templates/services/UpgradedServices.ts +++ b/core/templates/services/UpgradedServices.ts @@ -196,6 +196,8 @@ import { ExtensionTagAssemblerService } from import { ExtractImageFilenamesFromStateService } from // eslint-disable-next-line max-len 'pages/exploration-player-page/services/extract-image-filenames-from-state.service'; +import { FeaturedTranslationLanguageObjectFactory } from + 'domain/opportunity/FeaturedTranslationLanguageObjectFactory'; import { FeedbackMessageSummaryObjectFactory } from 'domain/feedback_message/FeedbackMessageSummaryObjectFactory'; import { FeedbackThreadObjectFactory } from @@ -645,6 +647,8 @@ export class UpgradedServices { upgradedServices['ExplorationOpportunitySummaryObjectFactory'] = new ExplorationOpportunitySummaryObjectFactory(); upgradedServices['ExpressionParserService'] = new ExpressionParserService(); + upgradedServices['FeaturedTranslationLanguageObjectFactory'] = + new FeaturedTranslationLanguageObjectFactory(); upgradedServices['FeedbackMessageSummaryObjectFactory'] = new FeedbackMessageSummaryObjectFactory(); upgradedServices['FeedbackThreadSummaryObjectFactory'] = @@ -1140,7 +1144,8 @@ export class UpgradedServices { upgradedServices['ContributionOpportunitiesBackendApiService'] = new ContributionOpportunitiesBackendApiService( upgradedServices['UrlInterpolationService'], - upgradedServices['HttpClient']); + upgradedServices['HttpClient'], + upgradedServices['FeaturedTranslationLanguageObjectFactory']); upgradedServices['CreatorDashboardBackendApiService'] = new CreatorDashboardBackendApiService(upgradedServices['HttpClient']); upgradedServices['CurrentInteractionService'] = diff --git a/core/tests/protractor_desktop/communityDashboard.js b/core/tests/protractor_desktop/communityDashboard.js index 3743ab762614..4fd12ec5ac3d 100644 --- a/core/tests/protractor_desktop/communityDashboard.js +++ b/core/tests/protractor_desktop/communityDashboard.js @@ -1,4 +1,4 @@ -// Copyright 2019 The Oppia Authors. All Rights Reserved. +// Copyright 2020 The Oppia Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ */ var general = require('../protractor_utils/general.js'); +var users = require('../protractor_utils/users.js'); +var waitFor = require('../protractor_utils/waitFor.js'); +var AdminPage = require('../protractor_utils/AdminPage.js'); var CommunityDashboardPage = require( '../protractor_utils/CommunityDashboardPage.js'); @@ -30,16 +33,50 @@ describe('Community dashboard page', function() { new CommunityDashboardPage.CommunityDashboardPage()); communityDashboardTranslateTextTab = ( communityDashboardPage.getTranslateTextTab()); + }); + + beforeEach(async function() { await browser.get('/community-dashboard'); + await waitFor.pageToFullyLoad(); + await communityDashboardPage.navigateToTranslateTextTab(); }); it('should allow user to switch to translate text tab', async function() { - await communityDashboardPage.navigateToTranslateTextTab(); await communityDashboardTranslateTextTab.changeLanguage('Hindi'); await communityDashboardTranslateTextTab.expectSelectedLanguageToBe( 'Hindi'); }); + describe('featured languages', () => { + beforeAll(async function() { + await users.createAndLoginAdminUser( + 'config@communityDashboard.com', 'communityDashboard'); + const adminPage = new AdminPage.AdminPage(); + await adminPage.editConfigProperty( + 'Featured Translation Languages', + 'List', + async function(elem) { + const featured = await elem.addItem('Dictionary'); + await (await featured.editEntry(0, 'Unicode')).setValue('fr'); + await (await featured.editEntry(1, 'Unicode')) + .setValue('Partnership with ABC'); + }); + await users.logout(); + }); + + it('should show correct featured languages', async function() { + await communityDashboardTranslateTextTab + .expectFeaturedLanguagesToBe(['French']); + }); + + it('should show correct explanation', async function() { + await communityDashboardTranslateTextTab + .mouseoverFeaturedLanguageTooltip(0); + await communityDashboardTranslateTextTab + .expectFeaturedLanguageExplanationToBe('Partnership with ABC'); + }); + }); + afterEach(async function() { await general.checkForConsoleErrors([]); }); diff --git a/core/tests/protractor_utils/AdminPage.js b/core/tests/protractor_utils/AdminPage.js index 220a70647688..755a13a0f860 100644 --- a/core/tests/protractor_utils/AdminPage.js +++ b/core/tests/protractor_utils/AdminPage.js @@ -140,10 +140,14 @@ var AdminPage = function() { await this.get(); await configTab.click(); await waitFor.elementToBeClickable(saveAllConfigs); - var results = await configProperties.map(async function(x) { - return await saveConfigProperty( - x, propertyName, objectType, editingInstructions); - }); + + const results = []; + for (let configProperty of (await configProperties)) { + results.push( + await saveConfigProperty( + configProperty, propertyName, objectType, editingInstructions) + ); + } var success = null; for (var i = 0; i < results.length; i++) { success = success || results[i]; diff --git a/core/tests/protractor_utils/CommunityDashboardTranslateTextTab.js b/core/tests/protractor_utils/CommunityDashboardTranslateTextTab.js index 8cedea0e0fc0..3ff8fac870b8 100644 --- a/core/tests/protractor_utils/CommunityDashboardTranslateTextTab.js +++ b/core/tests/protractor_utils/CommunityDashboardTranslateTextTab.js @@ -20,14 +20,36 @@ var waitFor = require('../protractor_utils/waitFor.js'); var CommunityDashboardTranslateTextTab = function() { - var selectableLanguageElements = element( + var selectorContainer = element( by.css('.protractor-test-language-selector')); - var selectedLanguageElement = selectableLanguageElements.element( - by.css('option:checked')); + var selectedLanguageElement = selectorContainer.element( + by.css('.protractor-test-language-selector-selected')); + var dropdown = selectorContainer.element( + by.css('.protractor-test-language-selector-dropdown')); + var featuredLanguageContainer = selectorContainer.element( + by.css('.protractor-test-featured-language-container')); + var featuredLanguageElements = selectorContainer.all( + by.css('.protractor-test-featured-language')); + var featuredLanguageTooltipElements = selectorContainer.all( + by.css('.protractor-test-featured-language-tooltip')); + var featuredLanguageExplanation = selectorContainer.element( + by.css('.protractor-test-language-selector-featured-explanation')); + + var _openLanguageSelector = async function() { + await waitFor.elementToBeClickable( + selectorContainer, + 'Language selector taking too long to be clickable' + ); + await selectorContainer.click(); + }; var _selectLanguage = async function(language) { - await selectableLanguageElements.element( - by.cssContainingText('option', language)).click(); + await _openLanguageSelector(); + await selectorContainer.element( + by.cssContainingText( + '.protractor-test-language-selector-option', + language + )).click(); }; this.changeLanguage = async function(language) { @@ -35,6 +57,38 @@ var CommunityDashboardTranslateTextTab = function() { await waitFor.pageToFullyLoad(); }; + this.mouseoverFeaturedLanguageTooltip = async function(index) { + await _openLanguageSelector(); + await waitFor.visibilityOf( + featuredLanguageContainer, + 'Featured languages took too long to display' + ); + await browser.actions().mouseMove( + featuredLanguageTooltipElements.get(index) + ).perform(); + }; + + this.expectFeaturedLanguagesToBe = async function(languages) { + await _openLanguageSelector(); + await waitFor.visibilityOf( + featuredLanguageContainer, + 'Featured languages took too long to display' + ); + var displayedFeaturedLanguages = await featuredLanguageElements + .map(async function(featuredLanguageElement) { + return (await featuredLanguageElement.getText()).replace('info\n', ''); + }); + expect(displayedFeaturedLanguages).toEqual(languages); + }; + + this.expectFeaturedLanguageExplanationToBe = async function(explanation) { + await waitFor.visibilityOf( + featuredLanguageExplanation, + 'featured language explanation took too long to show' + ); + expect(await featuredLanguageExplanation.getText()).toEqual(explanation); + }; + this.expectSelectedLanguageToBe = async function(language) { expect(await selectedLanguageElement.getText()).toMatch(language); }; diff --git a/main.py b/main.py index e0c01f81a696..f25ac19e44c6 100644 --- a/main.py +++ b/main.py @@ -265,6 +265,9 @@ def ui_access_wrapper(self, *args, **kwargs): get_redirect_route( r'/usercommunityrightsdatahandler', community_dashboard.UserCommunityRightsDataHandler), + get_redirect_route( + r'/retrivefeaturedtranslationlanguages', + community_dashboard.FeaturedTranslationLanguagesHandler), get_redirect_route( r'%s' % feconf.NEW_SKILL_URL, topics_and_skills_dashboard.NewSkillHandler), diff --git a/schema_utils.py b/schema_utils.py index bdf5ef6fcdbc..e0fa7225bd7b 100644 --- a/schema_utils.py +++ b/schema_utils.py @@ -33,6 +33,7 @@ from core.domain import expression_parser from core.domain import html_cleaner import python_utils +import utils SCHEMA_KEY_ITEMS = 'items' SCHEMA_KEY_LEN = 'len' @@ -447,3 +448,15 @@ def is_valid_math_equation(obj): if lhs_is_numerically_valid and rhs_is_algebraically_valid: return True return False + + @staticmethod + def is_supported_audio_language_code(obj): + """Checks if the given obj (a string) represents a valid language code. + + Args: + obj: str. A string. + + Returns: + bool. Whether the given object is a valid audio language code. + """ + return utils.is_supported_audio_language_code(obj) diff --git a/schema_utils_test.py b/schema_utils_test.py index c47374936dab..247b77884a8d 100644 --- a/schema_utils_test.py +++ b/schema_utils_test.py @@ -169,7 +169,8 @@ 'type': SCHEMA_TYPE_BOOL } }, - 'is_valid_math_equation': {} + 'is_valid_math_equation': {}, + 'is_supported_audio_language_code': {} }, } @@ -538,6 +539,18 @@ def test_is_valid_math_equation_validator(self): self.assertFalse(is_valid_math_equation('(a+(b)=0')) self.assertFalse(is_valid_math_equation('a+b=c:)')) + def test_is_supported_audio_language_code(self): + is_supported_audio_language_code = schema_utils.get_validator( + 'is_supported_audio_language_code') + + self.assertTrue(is_supported_audio_language_code('en')) + self.assertTrue(is_supported_audio_language_code('fr')) + self.assertTrue(is_supported_audio_language_code('de')) + + self.assertFalse(is_supported_audio_language_code('')) + self.assertFalse(is_supported_audio_language_code('zz')) + self.assertFalse(is_supported_audio_language_code('test')) + class SchemaNormalizationUnitTests(test_utils.GenericTestBase): """Test schema-based normalization of objects."""