-
Notifications
You must be signed in to change notification settings - Fork 0
[TM-2739] add pull push button translation forms #463
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
base: staging
Are you sure you want to change the base?
Changes from all commits
74cd2f9
c942a15
bfa3ae7
c3e9fce
8f7f69b
e9896ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,11 +20,16 @@ import { FormGetQueryDto, FormIndexQueryDto } from "./dto/form-query.dto"; | |
| import { JsonApiDeletedResponse } from "@terramatch-microservices/common/decorators/json-api-response.decorator"; | ||
| import { PolicyService } from "@terramatch-microservices/common"; | ||
| import { Form } from "@terramatch-microservices/database/entities"; | ||
| import { LocalizationService } from "@terramatch-microservices/common/localization/localization.service"; | ||
|
|
||
| @Controller("forms/v3/forms") | ||
| @ApiExtraModels(Forms) | ||
| export class FormsController { | ||
| constructor(private readonly formsService: FormsService, private readonly policyService: PolicyService) {} | ||
| constructor( | ||
| private readonly formsService: FormsService, | ||
| private readonly policyService: PolicyService, | ||
| private readonly localizationService: LocalizationService | ||
| ) {} | ||
|
|
||
| @Get() | ||
| @ApiOperation({ | ||
|
|
@@ -100,4 +105,27 @@ export class FormsController { | |
| await this.formsService.store(payload.data.attributes, form); | ||
| return await this.formsService.addFullDto(buildJsonApi<FormFullDto>(FormFullDto), form, false); | ||
| } | ||
|
|
||
| @Post(":uuid/translate") | ||
| @ApiOperation({ operationId: "formTranslate", description: "Translate a form" }) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint doesn't actually do the translation, it triggers the translation process, so this should probably reflect that. |
||
| @ExceptionResponse(UnauthorizedException, { description: "Form translation not allowed." }) | ||
| @ExceptionResponse(BadRequestException, { description: "Form payload malformed." }) | ||
| @ExceptionResponse(NotFoundException, { description: "Form not found." }) | ||
| async translate(@Param("uuid") uuid: string) { | ||
| const form = await this.formsService.findOne(uuid); | ||
| await this.policyService.authorize("update", form); | ||
| await this.localizationService.pushTranslationByForm(form); | ||
| return await this.formsService.addFullDto(buildJsonApi<FormFullDto>(FormFullDto), form, false); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifying the generic DTO type explicitly should not be needed here: I also think it's a little weird for this endpoint to return the form DTO. As mentioned above, you're creating a translation, not a form. Doing the work to generate the full form DTO doesn't really seem appropriate. Perhaps there should be a |
||
| } | ||
|
|
||
| @Get(":uuid/translate") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint should match the one above in terms of the name of the endpoint, description and DTO. For one thing, this endpoint currently doesn't return anything, which is a bit odd and would be new for this codebase. I think that when you create a form translation DTO for the endpoint above (I expect it'll be tiny, but I'm not sure what's really needed in it), it should be returned here as well. I haven't looked at how the FE implementation works yet, but for one thing, not returning anything at all is likely to cause looping requests in the connection system. |
||
| @ApiOperation({ operationId: "formPullTranslations", description: "Pull translations for a form" }) | ||
| @ExceptionResponse(UnauthorizedException, { description: "Form translation not allowed." }) | ||
| @ExceptionResponse(BadRequestException, { description: "Form payload malformed." }) | ||
| @ExceptionResponse(NotFoundException, { description: "Form not found." }) | ||
| async pullTranslations(@Param("uuid") uuid: string) { | ||
| const form = await this.formsService.findOne(uuid); | ||
| await this.policyService.authorize("update", form); | ||
| await this.localizationService.pullTranslations({ filterTags: form.uuid }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { Test } from "@nestjs/testing"; | ||
| import { LocalizationFormService } from "./localization-form.service"; | ||
| import { | ||
| FormFactory, | ||
| FormQuestionFactory, | ||
| FormSectionFactory, | ||
| I18nItemFactory | ||
| } from "@terramatch-microservices/database/factories"; | ||
|
|
||
| describe("LocalizationFormService", () => { | ||
| let service: LocalizationFormService; | ||
|
|
||
| beforeEach(async () => { | ||
| const module = await Test.createTestingModule({ | ||
| providers: [LocalizationFormService] | ||
| }).compile(); | ||
|
|
||
| service = module.get<LocalizationFormService>(LocalizationFormService); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| describe("getI18nIdsForForm", () => { | ||
| it("should return the I18n IDs for a form", async () => { | ||
| const i18nItem = await I18nItemFactory.create(); | ||
| const form = await FormFactory.create({ titleId: i18nItem.id }); | ||
| const formSection1 = await FormSectionFactory.create({ formId: form.uuid }); | ||
| const formSection2 = await FormSectionFactory.create({ formId: form.uuid }); | ||
| await FormQuestionFactory.create({ formSectionId: formSection1.id }); | ||
| await FormQuestionFactory.create({ formSectionId: formSection2.id }); | ||
| const i18nIds = await service.getI18nIdsForForm(form); | ||
| expect(i18nIds).toBeDefined(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { Injectable } from "@nestjs/common"; | ||
| import { | ||
| Form, | ||
| FormOptionList, | ||
| FormOptionListOption, | ||
| FormQuestion, | ||
| FormQuestionOption, | ||
| FormSection, | ||
| FormTableHeader, | ||
| FundingProgramme, | ||
| LocalizationKey | ||
| } from "@terramatch-microservices/database/entities"; | ||
| import { intersection } from "lodash"; | ||
| import { Model, Op } from "sequelize"; | ||
|
|
||
| type TranslationModelType = | ||
| | typeof Form | ||
| | typeof FormSection | ||
| | typeof FormQuestion | ||
| | typeof FormQuestionOption | ||
| | typeof FormTableHeader | ||
| | typeof FormOptionList | ||
| | typeof FundingProgramme | ||
| | typeof LocalizationKey | ||
| | typeof FormOptionListOption; | ||
|
|
||
| type TranslationParamsType = string | number; | ||
|
|
||
| @Injectable() | ||
| export class LocalizationFormService { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a good reason to make a whole new service instead of simply including these methods in the existing form service? |
||
| private extraFields: string[] = ["id", "optionsList"]; | ||
|
|
||
| private i18nIdsToBePushed: number[] = []; | ||
|
|
||
| private getI18nTranslationEntityFields(translationEntity: TranslationModelType) { | ||
| return translationEntity.I18N_FIELDS.map(field => `${field}Id`); | ||
| } | ||
|
|
||
| private async processTranslationEntity( | ||
| model: TranslationModelType, | ||
| property: string, | ||
| filterParams: TranslationParamsType | TranslationParamsType[] | ||
| ) { | ||
| const filterParamsArray = Array.isArray(filterParams) ? filterParams : [filterParams]; | ||
| const i18nFields = this.getI18nTranslationEntityFields(model); | ||
| // @ts-expect-error - entity is a model class | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your TS errors will probably go away and the code will be cleaner (for one thing, the return type will be defined so the error you're hiding in getI18nIdsForForm should go away) if you make this method generic: |
||
| const entities = await model.findAll({ | ||
| where: { | ||
| [property]: { | ||
| [Op.in]: filterParamsArray | ||
| } | ||
| }, | ||
| // @ts-expect-error - model is a model class | ||
| attributes: intersection(Object.keys(model.getAttributes()), [...i18nFields, ...this.extraFields]) | ||
| }); | ||
|
|
||
| entities.forEach(entity => { | ||
| this.pushI18nIds(entity, i18nFields); | ||
| }); | ||
|
|
||
| return entities; | ||
| } | ||
|
|
||
| private pushI18nIds(entity: Model, i18nFields: string[]) { | ||
| Object.entries(entity.dataValues).forEach(([key, value]) => { | ||
| if (i18nFields.includes(key) && value != null) { | ||
| this.i18nIdsToBePushed.push(value as number); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| async getI18nIdsForForm(form: Form) { | ||
| this.pushI18nIds(form, this.getI18nTranslationEntityFields(Form)); | ||
| const formSections = await this.processTranslationEntity(FormSection, "formId", [form.uuid]); | ||
| const formQuestions = await this.processTranslationEntity( | ||
| FormQuestion, | ||
| "formSectionId", | ||
| formSections.map(section => section.id) | ||
| ); | ||
| await this.processTranslationEntity( | ||
| FormTableHeader, | ||
| "formQuestionId", | ||
| formQuestions.map(question => question.id) | ||
| ); | ||
| const optionsListParams = formQuestions | ||
| // @ts-expect-error - optionsList is a field of FormQuestion | ||
| .map(question => question.optionsList) | ||
| .filter(optionsList => optionsList != null) | ||
| .filter(optionsList => optionsList != "0"); | ||
| const formOptionsLists = await this.processTranslationEntity(FormOptionList, "key", optionsListParams); | ||
| await this.processTranslationEntity( | ||
| FormOptionListOption, | ||
| "formOptionListId", | ||
| formOptionsLists.map(list => list.id) | ||
| ); | ||
| return this.i18nIdsToBePushed; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you're making a REST creation endpoint, you're creating a resource. This reads like an action (you don't create a
translate, you create atranslation), so this should likely be":uuid/translation"