diff --git a/DemographicDataPlugin.php b/DemographicDataPlugin.php index f47cf61..0b53ea4 100644 --- a/DemographicDataPlugin.php +++ b/DemographicDataPlugin.php @@ -33,6 +33,7 @@ public function register($category, $path, $mainContextId = null): bool Hook::add('Schema::get::author', [$this, 'editAuthorSchema']); Hook::add('Schema::get::demographicQuestion', [$this, 'addCustomSchema']); Hook::add('Schema::get::demographicResponse', [$this, 'addCustomSchema']); + Hook::add('Schema::get::demographicResponseOption', [$this, 'addCustomSchema']); Hook::add('Decision::add', [$this, 'requestDataExternalContributors']); Hook::add('User::edit', [$this, 'checkMigrateResponsesOrcid']); diff --git a/classes/DemographicDataService.php b/classes/DemographicDataService.php index 2aa9d48..e707b43 100644 --- a/classes/DemographicDataService.php +++ b/classes/DemographicDataService.php @@ -26,14 +26,19 @@ public function retrieveAllQuestions(int $contextId, bool $shouldRetrieveRespons 'inputType' => $demographicQuestion->getQuestionInputType(), 'title' => $demographicQuestion->getLocalizedQuestionText(), 'description' => $demographicQuestion->getLocalizedQuestionDescription(), - 'possibleResponses' => $demographicQuestion->getLocalizedPossibleResponses() + 'responseOptions' => $demographicQuestion->getResponseOptions() ]; + if ($demographicQuestion->getQuestionType() == DemographicQuestion::TYPE_DROP_DOWN_BOX) { + $questionData['responseOptions'] = []; + foreach ($demographicQuestion->getResponseOptions() as $responseOption) { + $questionData['responseOptions'][$responseOption->getId()] = $responseOption->getLocalizedOptionText(); + } + } + if ($shouldRetrieveResponses) { $user = $request->getUser(); - $response = $this->getUserResponse($demographicQuestion, $user->getId()); - - $questionData['response'] = $response; + $questionData['response'] = $this->getUserResponse($demographicQuestion, $user->getId()); } $questions[] = $questionData; @@ -56,17 +61,17 @@ private function getUserResponse(DemographicQuestion $question, int $userId) $question->getQuestionType() == DemographicQuestion::TYPE_CHECKBOXES || $question->getQuestionType() == DemographicQuestion::TYPE_RADIO_BUTTONS ) { - return []; + return ['value' => [], 'optionsInputValue' => []]; } - return null; + return ['value' => null]; } $firstResponse = array_shift($demographicResponses); - return $firstResponse->getValue(); + return ['value' => $firstResponse->getValue(), 'optionsInputValue' => $firstResponse->getOptionsInputValue()]; } - public function registerUserResponses(int $userId, array $responses) + public function registerUserResponses(int $userId, array $responses, array $responseOptionsInputs) { foreach ($responses as $question => $responseInput) { $questionId = explode("-", $question)[1]; @@ -77,19 +82,47 @@ public function registerUserResponses(int $userId, array $responses) ->getMany() ->toArray(); $demographicResponse = array_shift($demographicResponses); + + $optionsInputValue = $this->getResponseOptionsInputValue($questionId, $responseOptionsInputs, $responseInput); + if ($demographicResponse) { - Repo::demographicResponse()->edit($demographicResponse, ['responseValue' => $responseInput]); + Repo::demographicResponse()->edit($demographicResponse, [ + 'responseValue' => $responseInput, + 'optionsInputValue' => $optionsInputValue + ]); } else { $response = Repo::demographicResponse()->newDataObject(); $response->setUserId($userId); $response->setDemographicQuestionId($questionId); $response->setData('responseValue', $responseInput); + $response->setOptionsInputValue($optionsInputValue); Repo::demographicResponse()->add($response); } } } - public function registerExternalAuthorResponses(string $externalId, string $externalType, array $responses) + private function getResponseOptionsInputValue($questionId, $responseOptionsInputs, $responseInput) + { + $demographicQuestion = Repo::demographicQuestion()->get($questionId); + + if ($demographicQuestion->getQuestionType() == DemographicQuestion::TYPE_CHECKBOXES + || $demographicQuestion->getQuestionType() == DemographicQuestion::TYPE_RADIO_BUTTONS + ) { + $responseOptionsInputValue = []; + foreach ($responseInput as $responseOptionId) { + $responseOptionInputName = "responseOptionInput-$responseOptionId"; + if (isset($responseOptionsInputs[$responseOptionInputName])) { + $responseOptionsInputValue[$responseOptionId] = $responseOptionsInputs[$responseOptionInputName]; + } + } + + return $responseOptionsInputValue; + } + + return null; + } + + public function registerExternalAuthorResponses(string $externalId, string $externalType, array $responses, array $responseOptionsInputs) { $locale = Locale::getLocale(); @@ -98,10 +131,13 @@ public function registerExternalAuthorResponses(string $externalId, string $exte $questionId = $questionParts[1]; $questionType = $questionParts[2]; + $optionsInputValue = $this->getResponseOptionsInputValue($questionId, $responseOptionsInputs, $responseInput); + $response = Repo::demographicResponse()->newDataObject(); $response->setDemographicQuestionId($questionId); $response->setExternalId($externalId); $response->setExternalType($externalType); + $response->setOptionsInputValue($optionsInputValue); if ($questionType == 'text' or $questionType == 'textarea') { $response->setData('responseValue', $responseInput, $locale); @@ -144,21 +180,29 @@ private function getResponseValueForDisplay($question, $response): string $question->getQuestionType() == DemographicQuestion::TYPE_CHECKBOXES || $question->getQuestionType() == DemographicQuestion::TYPE_RADIO_BUTTONS ) { - $possibleResponses = $question->getLocalizedPossibleResponses(); - $selectedResponsesValues = []; + $responseOptions = $question->getResponseOptions(); + $selectedResponseOptionsTexts = []; + + foreach ($response->getValue() as $selectedResponseOptionId) { + $selectedResponseOption = $responseOptions[$selectedResponseOptionId]; + $selectedResponseOptionsText = $selectedResponseOption->getLocalizedOptionText(); + + if ($selectedResponseOption->hasInputField()) { + $optionsInputValue = $response->getOptionsInputValue(); + $selectedResponseOptionsText .= ' "' . $optionsInputValue[$selectedResponseOptionId] . '"'; + } - foreach ($response->getValue() as $selectedResponse) { - $selectedResponsesValues[] = $possibleResponses[$selectedResponse]; + $selectedResponseOptionsTexts[] = $selectedResponseOptionsText; } - return implode(', ', $selectedResponsesValues); + return implode(', ', $selectedResponseOptionsTexts); } if ($question->getQuestionType() == DemographicQuestion::TYPE_DROP_DOWN_BOX) { - $possibleResponses = $question->getLocalizedPossibleResponses(); - $selectedResponse = $response->getValue(); + $responseOptions = $question->getResponseOptions(); + $selectedResponseOption = $responseOptions[$response->getValue()]; - return $possibleResponses[$selectedResponse]; + return $selectedResponseOption->getLocalizedOptionText(); } return ''; diff --git a/classes/demographicQuestion/DemographicQuestion.php b/classes/demographicQuestion/DemographicQuestion.php index 43a98fa..08c1804 100644 --- a/classes/demographicQuestion/DemographicQuestion.php +++ b/classes/demographicQuestion/DemographicQuestion.php @@ -2,6 +2,8 @@ namespace APP\plugins\generic\demographicData\classes\demographicQuestion; +use APP\plugins\generic\demographicData\classes\facades\Repo; + class DemographicQuestion extends \PKP\core\DataObject { public const TYPE_SMALL_TEXT_FIELD = 1; @@ -77,18 +79,21 @@ public function setQuestionDescription($descriptionText, $locale) $this->setData('questionDescription', $descriptionText, $locale); } - public function getPossibleResponses($locale) + public function getResponseOptions() { - return $this->getData('possibleResponses', $locale); - } + if (is_null($this->getData('responseOptions'))) { + $responseOptions = Repo::demographicResponseOption()->getCollector() + ->filterByQuestionIds([$this->getId()]) + ->getMany(); - public function setPossibleResponses($possibleResponses, $locale) - { - $this->setData('possibleResponses', $possibleResponses, $locale); - } + $mappedResponseOptions = []; + foreach ($responseOptions as $responseOption) { + $mappedResponseOptions[$responseOption->getId()] = $responseOption; + } - public function getLocalizedPossibleResponses() - { - return $this->getLocalizedData('possibleResponses'); + $this->setData('responseOptions', $mappedResponseOptions); + } + + return $this->getData('responseOptions'); } } diff --git a/classes/demographicResponse/DAO.php b/classes/demographicResponse/DAO.php index 74b6799..ea60720 100644 --- a/classes/demographicResponse/DAO.php +++ b/classes/demographicResponse/DAO.php @@ -76,6 +76,11 @@ public function fromRow(object $row): DemographicResponse $demographicResponse->setValue(unserialize($serializedValue)); } + if (@unserialize($demographicResponse->getOptionsInputValue())) { + $serializedValue = $demographicResponse->getOptionsInputValue(); + $demographicResponse->setOptionsInputValue(unserialize($serializedValue)); + } + return $demographicResponse; } } diff --git a/classes/demographicResponse/DemographicResponse.php b/classes/demographicResponse/DemographicResponse.php index 207f0fb..99617a7 100644 --- a/classes/demographicResponse/DemographicResponse.php +++ b/classes/demographicResponse/DemographicResponse.php @@ -53,4 +53,14 @@ public function setValue($responseValue) { $this->setData('responseValue', $responseValue); } + + public function getOptionsInputValue() + { + return $this->getData('optionsInputValue'); + } + + public function setOptionsInputValue($optionsInputValue) + { + $this->setData('optionsInputValue', $optionsInputValue); + } } diff --git a/classes/demographicResponseOption/Collector.php b/classes/demographicResponseOption/Collector.php new file mode 100644 index 0000000..0cb2062 --- /dev/null +++ b/classes/demographicResponseOption/Collector.php @@ -0,0 +1,52 @@ +dao = $dao; + } + + public function filterByQuestionIds(?array $questionIds): Collector + { + $this->questionIds = $questionIds; + return $this; + } + + public function getQueryBuilder(): Builder + { + $queryBuilder = DB::table($this->dao->table . ' AS dro') + ->select(['dro.*']); + + if (isset($this->questionIds)) { + $queryBuilder->whereIn('dro.demographic_question_id', $this->questionIds); + } + + return $queryBuilder; + } + + public function getCount(): int + { + return $this->dao->getCount($this); + } + + public function getIds(): Collection + { + return $this->dao->getIds($this); + } + + public function getMany(): LazyCollection + { + return $this->dao->getMany($this); + } +} diff --git a/classes/demographicResponseOption/DAO.php b/classes/demographicResponseOption/DAO.php new file mode 100644 index 0000000..2787c1c --- /dev/null +++ b/classes/demographicResponseOption/DAO.php @@ -0,0 +1,71 @@ + 'demographic_response_option_id', + 'demographicQuestionId' => 'demographic_question_id', + ]; + + public function getParentColumn(): string + { + return 'demographic_question_id'; + } + + public function newDataObject(): DemographicResponseOption + { + return app(DemographicResponseOption::class); + } + + public function insert(DemographicResponseOption $demographicResponseOption): int + { + return parent::_insert($demographicResponseOption); + } + + public function delete(DemographicResponseOption $demographicResponseOption) + { + return parent::_delete($demographicResponseOption); + } + + public function update(DemographicResponseOption $demographicResponseOption) + { + return parent::_update($demographicResponseOption); + } + + public function getCount(Collector $query): int + { + return $query + ->getQueryBuilder() + ->count(); + } + + public function getMany(Collector $query): LazyCollection + { + $rows = $query + ->getQueryBuilder() + ->get(); + + return LazyCollection::make(function () use ($rows) { + foreach ($rows as $row) { + yield $row->demographic_response_option_id => $this->fromRow($row); + } + }); + } + + public function fromRow(object $row): DemographicResponseOption + { + return parent::fromRow($row); + } +} diff --git a/classes/demographicResponseOption/DemographicResponseOption.php b/classes/demographicResponseOption/DemographicResponseOption.php new file mode 100644 index 0000000..d889985 --- /dev/null +++ b/classes/demographicResponseOption/DemographicResponseOption.php @@ -0,0 +1,36 @@ +getData('demographicQuestionId'); + } + + public function setDemographicQuestionId($demographicQuestionId) + { + $this->setData('demographicQuestionId', $demographicQuestionId); + } + + public function getLocalizedOptionText() + { + return $this->getLocalizedData('optionText'); + } + + public function setOptionText(string $text, string $locale) + { + $this->setData('optionText', $text, $locale); + } + + public function hasInputField(): bool + { + return $this->getData('hasInputField'); + } + + public function setHasInputField(bool $hasInputField) + { + $this->setData('hasInputField', $hasInputField); + } +} diff --git a/classes/demographicResponseOption/Repository.php b/classes/demographicResponseOption/Repository.php new file mode 100644 index 0000000..810d443 --- /dev/null +++ b/classes/demographicResponseOption/Repository.php @@ -0,0 +1,58 @@ +dao = $dao; + } + + public function newDataObject(array $params = []): DemographicResponseOption + { + $object = $this->dao->newDataObject(); + if (!empty($params)) { + $object->setAllData($params); + } + return $object; + } + + public function get(int $id, int $demographicQuestionId = null): ?DemographicResponseOption + { + return $this->dao->get($id, $demographicQuestionId); + } + + public function add(DemographicResponseOption $demographicResponseOption): int + { + $id = $this->dao->insert($demographicResponseOption); + return $id; + } + + public function edit(DemographicResponseOption $demographicResponseOption, array $params) + { + $newDemographicResponseOption = clone $demographicResponseOption; + $newDemographicResponseOption->setAllData(array_merge($newDemographicResponseOption->_data, $params)); + + $this->dao->update($newDemographicResponseOption); + } + + public function delete(DemographicResponseOption $demographicResponseOption) + { + $this->dao->delete($demographicResponseOption); + } + + public function exists(int $id, int $demographicQuestionId = null): bool + { + return $this->dao->exists($id, $demographicQuestionId); + } + + public function getCollector(): Collector + { + return app(Collector::class); + } +} diff --git a/classes/facades/Repo.php b/classes/facades/Repo.php index f5fc9d4..f8fc41c 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -4,6 +4,7 @@ use APP\plugins\generic\demographicData\classes\demographicQuestion\Repository as DemographicQuestionRepository; use APP\plugins\generic\demographicData\classes\demographicResponse\Repository as DemographicResponseRepository; +use APP\plugins\generic\demographicData\classes\demographicResponseOption\Repository as DemographicResponseOptionRepository; class Repo extends \APP\facades\Repo { @@ -16,4 +17,9 @@ public static function demographicResponse(): DemographicResponseRepository { return app(DemographicResponseRepository::class); } + + public static function demographicResponseOption(): DemographicResponseOptionRepository + { + return app(DemographicResponseOptionRepository::class); + } } diff --git a/classes/form/QuestionsForm.php b/classes/form/QuestionsForm.php index 0270b2f..f2945a5 100644 --- a/classes/form/QuestionsForm.php +++ b/classes/form/QuestionsForm.php @@ -35,13 +35,18 @@ public function __construct($request = null, $args = null) private function loadQuestionResponsesByForm($args) { $responses = []; + $responseOptionsInputs = []; + foreach ($args as $key => $value) { if (strpos($key, 'question-') === 0) { $responses[$key] = $value; + } elseif (strpos($key, 'responseOptionInput-') === 0) { + $responseOptionsInputs[$key] = $value; } } $this->setData('responses', $responses); + $this->setData('responseOptionsInputs', $responseOptionsInputs); } public function fetch($request, $template = null, $display = false) @@ -98,7 +103,7 @@ public function execute(...$functionArgs) $demographicDataDao->updateDemographicConsent($context->getId(), $user->getId(), $newConsent); if ($newConsent == '1') { - $demographicDataService->registerUserResponses($user->getId(), $this->getData('responses')); + $demographicDataService->registerUserResponses($user->getId(), $this->getData('responses'), $this->getData('responseOptionsInputs')); } elseif ($newConsent == '0' and $previousConsent) { $demographicDataService->deleteUserResponses($user->getId(), $context->getId()); } diff --git a/classes/migrations/SchemaMigration.php b/classes/migrations/SchemaMigration.php index cecfe54..6141197 100644 --- a/classes/migrations/SchemaMigration.php +++ b/classes/migrations/SchemaMigration.php @@ -29,12 +29,42 @@ public function up(): void $table->string('setting_name', 255); $table->longText('setting_value')->nullable(); + $table->foreign('demographic_question_id') + ->references('demographic_question_id') + ->on('demographic_questions') + ->onDelete('cascade'); $table->index(['demographic_question_id'], 'demographic_question_settings_id'); $table->unique(['demographic_question_id', 'locale', 'setting_name'], 'demographic_question_settings_pkey'); }); + Schema::create('demographic_response_options', function (Blueprint $table) { + $table->bigInteger('demographic_response_option_id')->autoIncrement(); + $table->bigInteger('demographic_question_id'); + + $table->foreign('demographic_question_id') + ->references('demographic_question_id') + ->on('demographic_questions') + ->onDelete('cascade'); + $table->index(['demographic_question_id'], 'demographic_response_options_demographic_question_id'); + }); + + Schema::create('demographic_response_option_settings', function (Blueprint $table) { + $table->bigIncrements('demographic_response_option_setting_id'); + $table->bigInteger('demographic_response_option_id'); + $table->string('locale', 14)->default(''); + $table->string('setting_name', 255); + $table->longText('setting_value')->nullable(); + + $table->foreign('demographic_response_option_id', 'demographic_response_option_settings_option_id') + ->references('demographic_response_option_id') + ->on('demographic_response_options') + ->onDelete('cascade'); + $table->index(['demographic_response_option_id'], 'demographic_response_option_settings_option_id'); + $table->unique(['demographic_response_option_id', 'locale', 'setting_name'], 'demographic_response_option_settings_pkey'); + }); + Schema::create('demographic_responses', function (Blueprint $table) { - $table->bigIncrements('demographic_response_id'); + $table->bigInteger('demographic_response_id')->autoIncrement(); $table->bigInteger('demographic_question_id'); $table->bigInteger('user_id')->nullable(); $table->string('external_id', 255)->nullable(); @@ -60,6 +90,10 @@ public function up(): void $table->string('setting_name', 255); $table->longText('setting_value')->nullable(); + $table->foreign('demographic_response_id') + ->references('demographic_response_id') + ->on('demographic_responses') + ->onDelete('cascade'); $table->index(['demographic_response_id'], 'demographic_response_setting_id'); $table->unique(['demographic_response_id', 'locale', 'setting_name'], 'demographic_response_settings_pkey'); }); diff --git a/classes/test/DefaultTestQuestionsCreator.php b/classes/test/DefaultTestQuestionsCreator.php index d6f8c69..db551fd 100644 --- a/classes/test/DefaultTestQuestionsCreator.php +++ b/classes/test/DefaultTestQuestionsCreator.php @@ -27,7 +27,15 @@ public function createDefaultTestQuestions() foreach ($defaultTestQuestions as $questionData) { $questionObject = Repo::demographicQuestion()->newDataObject($questionData); - Repo::demographicQuestion()->add($questionObject); + $demographicQuestionId = Repo::demographicQuestion()->add($questionObject); + + if (isset($questionData['responseOptions'])) { + foreach ($questionData['responseOptions'] as $optionData) { + $optionData['demographicQuestionId'] = $demographicQuestionId; + $responseOptionObject = Repo::demographicResponseOption()->newDataObject($optionData); + Repo::demographicResponseOption()->add($responseOptionObject); + } + } } } } @@ -58,9 +66,35 @@ private function getDefaultTestQuestionsData(int $contextId): array 'questionType' => DemographicQuestion::TYPE_CHECKBOXES, 'questionText' => ['en' => 'Languages'], 'questionDescription' => ['en' => 'Which of these languages do you speak?'], - 'possibleResponses' => [ - 'en' => ['English', 'French', 'Hindi', 'Mandarin', 'Portuguese', 'Spanish'], - 'fr_CA' => ['Anglais', 'Français', 'Hindi', 'Mandarin', 'Portugais', 'Espagnol'] + 'responseOptions' => [ + [ + 'optionText' => ['en' => 'English', 'fr_CA' => 'Anglais'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'French', 'fr_CA' => 'Français'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Hindi', 'fr_CA' => 'Hindi'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Mandarin', 'fr_CA' => 'Mandarin'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Portuguese', 'fr_CA' => 'Portugais'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Spanish', 'fr_CA' => 'Espagnol'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Other:', 'fr_CA' => 'Autre:'], + 'hasInputField' => true + ] ] ], [ @@ -68,9 +102,27 @@ private function getDefaultTestQuestionsData(int $contextId): array 'questionType' => DemographicQuestion::TYPE_RADIO_BUTTONS, 'questionText' => ['en' => 'Nacionality'], 'questionDescription' => ['en' => 'Which continent are you from?'], - 'possibleResponses' => [ - 'en' => ['Africa', 'America', 'Asia', 'Europe', 'Oceania'], - 'fr_CA' => ['Afrique', 'Amérique', 'Asie', 'Europe', 'Océanie'] + 'responseOptions' => [ + [ + 'optionText' => ['en' => 'Africa', 'fr_CA' => 'Afrique'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'America', 'fr_CA' => 'Amérique'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Asia', 'fr_CA' => 'Asie'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Europe', 'fr_CA' => 'Europe'], + 'hasInputField' => false + ], + [ + 'optionText' => ['en' => 'Oceania', 'fr_CA' => 'Océanie'], + 'hasInputField' => false + ] ] ], [ @@ -78,18 +130,34 @@ private function getDefaultTestQuestionsData(int $contextId): array 'questionType' => DemographicQuestion::TYPE_DROP_DOWN_BOX, 'questionText' => ['en' => 'Salary'], 'questionDescription' => ['en' => 'What range is your current salary in?'], - 'possibleResponses' => [ - 'en' => [ - 'Less than a minimum wage', - 'One to three minimum wages', - 'Three to five minimum wages', - 'More than five minimum wages' + 'responseOptions' => [ + [ + 'optionText' => [ + 'en' => 'Less than a minimum wage', + 'fr_CA' => "Moins qu'un salaire minimum" + ], + 'hasInputField' => false + ], + [ + 'optionText' => [ + 'en' => 'One to three minimum wages', + 'fr_CA' => 'Un à trois salaires minimums' + ], + 'hasInputField' => false + ], + [ + 'optionText' => [ + 'en' => 'Three to five minimum wages', + 'fr_CA' => 'Trois à cinq salaires minimums' + ], + 'hasInputField' => false ], - 'fr_CA' => [ - "Moins qu'un salaire minimum", - 'Un à trois salaires minimums', - 'Trois à cinq salaires minimums', - 'Plus de cinq salaires minimums' + [ + 'optionText' => [ + 'en' => 'More than five minimum wages', + 'fr_CA' => 'Plus de cinq salaires minimums' + ], + 'hasInputField' => false ] ] ] diff --git a/cypress/tests/Test1_questionsDisplaying.cy.js b/cypress/tests/Test1_questionsDisplaying.cy.js index 6fb70bd..c3e2690 100644 --- a/cypress/tests/Test1_questionsDisplaying.cy.js +++ b/cypress/tests/Test1_questionsDisplaying.cy.js @@ -17,6 +17,7 @@ function assertDefaultQuestionsDisplay() { cy.contains('Mandarin'); cy.contains('Portuguese'); cy.contains('Spanish'); + cy.contains('Other:'); cy.contains('label', 'Nacionality'); cy.contains('.description', 'Which continent are you from?'); @@ -47,6 +48,10 @@ function answerDefaultQuestions() { cy.contains('label', 'Spanish').within(() => { cy.get('input').check(); }); + cy.contains('label', 'Other:').parent().parent().within(() => { + cy.get('input[type="checkbox"]').check(); + cy.get('input[type="text"]').clear().type('Japanese'); + }); cy.contains('label', 'America').within(() => { cy.get('input').check(); }); @@ -70,6 +75,10 @@ function assertResponsesToDefaultQuestions() { cy.contains('label', 'Spanish').within(() => { cy.get('input').should('be.checked'); }); + cy.contains('label', 'Other:').parent().parent().within(() => { + cy.get('input[type="checkbox"]').should('be.checked'); + cy.get('input[type="text"]').should('have.value', 'Japanese'); + }); cy.contains('label', 'America').within(() => { cy.get('input').should('be.checked'); }); @@ -83,6 +92,10 @@ function assertResponsesToQuestionsInFrench() { cy.contains('label', 'Espagnol').within(() => { cy.get('input').should('be.checked'); }); + cy.contains('label', 'Autre:').parent().parent().within(() => { + cy.get('input[type="checkbox"]').should('be.checked'); + cy.get('input[type="text"]').should('have.value', 'Japanese'); + }); cy.contains('label', 'Amérique').within(() => { cy.get('input').should('be.checked'); }); diff --git a/cypress/tests/Test2_externalContributors.cy.js b/cypress/tests/Test2_externalContributors.cy.js index 37b26e1..e048794 100644 --- a/cypress/tests/Test2_externalContributors.cy.js +++ b/cypress/tests/Test2_externalContributors.cy.js @@ -51,6 +51,10 @@ function answerDefaultQuestions() { cy.contains('label', 'Spanish').within(() => { cy.get('input').check(); }); + cy.contains('label', 'Other:').parent().within(() => { + cy.get('input[type="checkbox"]').check(); + cy.get('input[type="text"]').clear().type('Japanese'); + }); cy.contains('label', 'America').within(() => { cy.get('input').check(); }); @@ -66,7 +70,7 @@ function assertResponsesOfExternalAuthor(authorEmail) { cy.contains('Latin'); cy.contains('University of São Paulo'); cy.contains('University of Minas Gerais'); - cy.contains('English, Spanish'); + cy.contains('English, Spanish, Other: "Japanese"'); cy.contains('America'); cy.contains('Three to five minimum wages'); @@ -88,6 +92,10 @@ function assertResponsesOfRegisteredUser() { cy.contains('label', 'Spanish').within(() => { cy.get('input').should('be.checked'); }); + cy.contains('label', 'Other:').parent().parent().within(() => { + cy.get('input[type="checkbox"]').check(); + cy.get('input[type="text"]').clear().type('Japanese'); + }); cy.contains('label', 'America').within(() => { cy.get('input').should('be.checked'); }); diff --git a/pages/demographic/QuestionnaireHandler.php b/pages/demographic/QuestionnaireHandler.php index 8a39467..fb311d3 100644 --- a/pages/demographic/QuestionnaireHandler.php +++ b/pages/demographic/QuestionnaireHandler.php @@ -112,9 +112,12 @@ public function saveQuestionnaire($args, $request) } $responses = []; + $responseOptionsInputs = []; foreach ($request->getUserVars() as $key => $value) { if (strpos($key, 'question-') === 0) { $responses[$key] = $value; + } elseif (strpos($key, 'responseOptionInput-') === 0) { + $responseOptionsInputs[$key] = $value; } } @@ -127,7 +130,7 @@ public function saveQuestionnaire($args, $request) } $demographicDataService = new DemographicDataService(); - $demographicDataService->registerExternalAuthorResponses($responsesExternalId, $responsesExternalType, $responses); + $demographicDataService->registerExternalAuthorResponses($responsesExternalId, $responsesExternalType, $responses, $responseOptionsInputs); $templateMgr->assign([ 'authorId' => $author->getId(), diff --git a/schemas/demographicQuestion.json b/schemas/demographicQuestion.json index 63e9a7d..a030ec2 100644 --- a/schemas/demographicQuestion.json +++ b/schemas/demographicQuestion.json @@ -19,16 +19,6 @@ "questionDescription": { "type": "string", "multilingual": true - }, - "possibleResponses": { - "type": "array", - "items": { - "type": "string" - }, - "multilingual": true, - "validation": [ - "nullable" - ] } } } diff --git a/schemas/demographicResponse.json b/schemas/demographicResponse.json index d6deede..808cd4a 100644 --- a/schemas/demographicResponse.json +++ b/schemas/demographicResponse.json @@ -32,6 +32,12 @@ "validation": [ "nullable" ] + }, + "optionsInputValue": { + "type": "string", + "validation": [ + "nullable" + ] } } } diff --git a/schemas/demographicResponseOption.json b/schemas/demographicResponseOption.json new file mode 100644 index 0000000..71ff3ed --- /dev/null +++ b/schemas/demographicResponseOption.json @@ -0,0 +1,20 @@ +{ + "title": "Demographic response option", + "description": "A response option for a given demographic question", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "demographicQuestionId": { + "type": "integer" + }, + "optionText": { + "type": "string", + "multilingual": true + }, + "hasInputField": { + "type": "boolean" + } + } +} diff --git a/styles/questionsInProfile.css b/styles/questionsInProfile.css new file mode 100644 index 0000000..bda628e --- /dev/null +++ b/styles/questionsInProfile.css @@ -0,0 +1,8 @@ +.responseOption { + display: flex; +} + +.responseOption input[type="text"] { + margin-left: 1rem; + height: 1.5rem; +} \ No newline at end of file diff --git a/templates/question.tpl b/templates/question.tpl index 7f5869a..1a1fd8a 100644 --- a/templates/question.tpl +++ b/templates/question.tpl @@ -1,3 +1,5 @@ + + {assign var="questionId" value="question-{$question['questionId']}-{$question['inputType']}"} {if $question['type'] == $questionTypeConsts['TYPE_CHECKBOXES'] or $question['type'] == $questionTypeConsts['TYPE_RADIO_BUTTONS']} {assign var="isListSection" value=true} @@ -6,38 +8,64 @@ {fbvFormSection title=$question['title'] required=true translate=false} {fbvFormSection for=$questionId description=$question['description'] translate=false list=$isListSection} {if $question['type'] == $questionTypeConsts['TYPE_SMALL_TEXT_FIELD']} - {fbvElement type="text" multilingual="true" name=$questionId id="demographicResponses" value=$question['response'] required=true size=$fbvStyles.size.SMALL} + {fbvElement type="text" multilingual="true" name=$questionId id="demographicResponses" value=$question['response']['value'] required=true size=$fbvStyles.size.SMALL} {elseif $question['type'] == $questionTypeConsts['TYPE_TEXT_FIELD']} - {fbvElement type="text" multilingual="true" name=$questionId id="demographicResponses" value=$question['response'] required=true size=$fbvStyles.size.LARGE} + {fbvElement type="text" multilingual="true" name=$questionId id="demographicResponses" value=$question['response']['value'] required=true size=$fbvStyles.size.LARGE} {elseif $question['type'] == $questionTypeConsts['TYPE_TEXTAREA']} - {fbvElement type="textarea" multilingual="true" name=$questionId id="demographicResponses" value=$question['response'] required=true rich=false size=$fbvStyles.size.LARGE} + {fbvElement type="textarea" multilingual="true" name=$questionId id="demographicResponses" value=$question['response']['value'] required=true rich=false size=$fbvStyles.size.LARGE} {elseif $question['type'] == $questionTypeConsts['TYPE_CHECKBOXES']} - {foreach from=$question['possibleResponses'] key="possibleResponseValue" item="possibleResponseLabel"} - {fbvElement - type="checkbox" - name="{$questionId}[]" - id="demographicResponses" - label=$possibleResponseLabel - value=$possibleResponseValue - checked=in_array($possibleResponseValue, $question['response']) - translate=false - } + {foreach from=$question['responseOptions'] item="responseOption"} +
+ {fbvElement + type="checkbox" + name="{$questionId}[]" + id="demographicResponses" + label=$responseOption->getLocalizedOptionText() + value=$responseOption->getId() + checked=in_array($responseOption->getId(), $question['response']['value']) + translate=false + } + + {if $responseOption->hasInputField()} + {assign var="optionInputName" value="responseOptionInput-{$responseOption->getId()}"} + {if isset($question['response']['optionsInputValue'][$responseOption->getId()])} + {assign var="optionInputValue" value=$question['response']['optionsInputValue'][$responseOption->getId()]} + {else} + {assign var="optionInputValue" value=""} + {/if} + + {fbvElement type="text" name=$optionInputName id="responseOptionsInputs" value=$optionInputValue size=$fbvStyles.size.MEDIUM} + {/if} +
{/foreach} {elseif $question['type'] == $questionTypeConsts['TYPE_RADIO_BUTTONS']} - {foreach from=$question['possibleResponses'] key="possibleResponseValue" item="possibleResponseLabel"} - {fbvElement - type="radio" - name="{$questionId}[]" - id="demographicResponses" - label=$possibleResponseLabel - value=$possibleResponseValue - checked=in_array($possibleResponseValue, $question['response']) - required=true - translate=false - } + {foreach from=$question['responseOptions'] item="responseOption"} +
+ {fbvElement + type="radio" + name="{$questionId}[]" + id="demographicResponses" + label=$responseOption->getLocalizedOptionText() + value=$responseOption->getId() + checked=in_array($responseOption->getId(), $question['response']['value']) + required=true + translate=false + } + + {if $responseOption->hasInputField()} + {assign var="optionInputName" value="responseOptionInput-{$responseOption->getId()}"} + {if isset($question['response']['optionsInputValue'][$responseOption->getId()])} + {assign var="optionInputValue" value=$question['response']['optionsInputValue'][$responseOption->getId()]} + {else} + {assign var="optionInputValue" value=""} + {/if} + + {fbvElement type="text" name=$optionInputName id="responseOptionsInputs" value=$optionInputValue size=$fbvStyles.size.MEDIUM} + {/if} +
{/foreach} {elseif $question['type'] == $questionTypeConsts['TYPE_DROP_DOWN_BOX']} - {fbvElement type="select" name=$questionId id="demographicResponses" from=$question['possibleResponses'] selected=$question['response'] translate=false required=true size=$fbvStyles.size.LARGE} + {fbvElement type="select" name=$questionId id="demographicResponses" from=$question['responseOptions'] selected=$question['response']['value'] translate=false required=true size=$fbvStyles.size.LARGE} {/if} {/fbvFormSection} {/fbvFormSection} \ No newline at end of file diff --git a/templates/questionnairePage/question.tpl b/templates/questionnairePage/question.tpl index 001acb8..50cc0ed 100644 --- a/templates/questionnairePage/question.tpl +++ b/templates/questionnairePage/question.tpl @@ -10,23 +10,37 @@ {elseif $question['type'] == $questionTypeConsts['TYPE_TEXTAREA']} {elseif $question['type'] == $questionTypeConsts['TYPE_CHECKBOXES']} - {foreach from=$question['possibleResponses'] key="possibleResponseValue" item="possibleResponseLabel"} -
+ {foreach from=$question['responseOptions'] item="responseOption"} +
+ + {if $responseOption->hasInputField()} + {assign var="optionInputName" value="responseOptionInput-{$responseOption->getId()}"} + + {/if} +
+
{/foreach} {elseif $question['type'] == $questionTypeConsts['TYPE_RADIO_BUTTONS']} - {foreach from=$question['possibleResponses'] key="possibleResponseValue" item="possibleResponseLabel"} -
+ {foreach from=$question['responseOptions'] item="responseOption"} +
+ + {if $responseOption->hasInputField()} + {assign var="optionInputName" value="responseOptionInput-{$responseOption->getId()}"} + + {/if} +
+
{/foreach} {elseif $question['type'] == $questionTypeConsts['TYPE_DROP_DOWN_BOX']} {/if} diff --git a/tests/demographicQuestion/DAOTest.php b/tests/demographicQuestion/DAOTest.php index 43615bb..0ace3f0 100644 --- a/tests/demographicQuestion/DAOTest.php +++ b/tests/demographicQuestion/DAOTest.php @@ -56,10 +56,7 @@ public function testCreateDemographicQuestion(): void 'contextId' => $this->contextId, 'questionType' => DemographicQuestion::TYPE_RADIO_BUTTONS, 'questionText' => [$locale => 'Test text'], - 'questionDescription' => [$locale => 'Test description'], - 'possibleResponses' => [ - $locale => ['First possible response', 'Second possible response'] - ] + 'questionDescription' => [$locale => 'Test description'] ], $fetchedDemographicQuestion->_data); } @@ -109,7 +106,6 @@ private function createDemographicQuestionObject($locale) $demographicQuestion->setQuestionType(DemographicQuestion::TYPE_RADIO_BUTTONS); $demographicQuestion->setQuestionText('Test text', $locale); $demographicQuestion->setQuestionDescription('Test description', $locale); - $demographicQuestion->setPossibleResponses(['First possible response', 'Second possible response'], $locale); return $demographicQuestion; } diff --git a/tests/demographicQuestion/DemographicQuestionTest.php b/tests/demographicQuestion/DemographicQuestionTest.php index 03de375..d177959 100644 --- a/tests/demographicQuestion/DemographicQuestionTest.php +++ b/tests/demographicQuestion/DemographicQuestionTest.php @@ -71,18 +71,4 @@ public function testGetQuestionDescription(): void $questionDescription = $this->demographicQuestion->getLocalizedQuestionDescription(); $this->assertEquals($expectedQuestionDescription, $questionDescription); } - - public function testGetQuestionPossibleResponses(): void - { - $expectedPossibleResponses = [ - 'en' => ['Black', 'Latin', 'Asian', 'Other'], - 'pt_BR' => ['Negro(a)', 'Latino(a)', 'Asiático(a)', 'Outro(a)'] - ]; - - $this->demographicQuestion->setPossibleResponses($expectedPossibleResponses['en'], 'en'); - $this->demographicQuestion->setPossibleResponses($expectedPossibleResponses['pt_BR'], 'pt_BR'); - - $this->assertEquals($expectedPossibleResponses['en'], $this->demographicQuestion->getPossibleResponses('en')); - $this->assertEquals($expectedPossibleResponses['pt_BR'], $this->demographicQuestion->getPossibleResponses('pt_BR')); - } } diff --git a/tests/demographicQuestion/RepositoryTest.php b/tests/demographicQuestion/RepositoryTest.php index ed49d70..9602218 100644 --- a/tests/demographicQuestion/RepositoryTest.php +++ b/tests/demographicQuestion/RepositoryTest.php @@ -39,9 +39,6 @@ protected function setUp(): void ], 'questionDescription' => [ $this->locale => 'Test description' - ], - 'possibleResponses' => [ - $this->locale => ['First possible response', 'Second possible response'] ] ]; $this->addSchemaFile('demographicQuestion'); @@ -68,7 +65,6 @@ public function testCrud(): void $this->params['questionText'][$this->locale] = 'Updated text'; $this->params['questionDescription'][$this->locale] = 'Updated description'; - $this->params['possibleResponses'][$this->locale] = ['New first possible response', 'New second possible response']; $repository->edit($demographicQuestion, $this->params); $fetchedDemographicQuestion = $repository->get($demographicQuestion->getId(), $this->contextId); diff --git a/tests/demographicResponse/DAOTest.php b/tests/demographicResponse/DAOTest.php index 31f259b..c1ded02 100644 --- a/tests/demographicResponse/DAOTest.php +++ b/tests/demographicResponse/DAOTest.php @@ -58,6 +58,7 @@ public function testCreateDemographicResponse(): void 'id' => $insertedDemographicResponseId, 'demographicQuestionId' => $this->demographicQuestionId, 'responseValue' => [self::DEFAULT_LOCALE => 'Test text'], + 'optionsInputValue' => [45 => 'Aditional information for response option'], 'userId' => $this->userId, 'externalId' => null, 'externalType' => null @@ -78,6 +79,7 @@ public function testCreateDemographicResponseForExternalAuthor(): void 'id' => $insertedDemographicResponseId, 'demographicQuestionId' => $this->demographicQuestionId, 'responseValue' => [self::DEFAULT_LOCALE => 'Test text'], + 'optionsInputValue' => [45 => 'Aditional information for response option'], 'userId' => null, 'externalId' => 'external.author@lepidus.com.br', 'externalType' => 'email' diff --git a/tests/demographicResponse/DemographicResponseTest.php b/tests/demographicResponse/DemographicResponseTest.php index 814a5fe..752b36d 100644 --- a/tests/demographicResponse/DemographicResponseTest.php +++ b/tests/demographicResponse/DemographicResponseTest.php @@ -49,4 +49,11 @@ public function testGetDemographicResponseValue(): void $this->demographicResponse->setValue(['en' => "I'm from Parintins"]); $this->assertEquals($this->demographicResponse->getValue(), $expectedDemographicResponseValue); } + + public function testGetDemographicOptionsInputValue(): void + { + $expectedOptionsInputValue = [45 => 'Aditional information for response option']; + $this->demographicResponse->setOptionsInputValue([45 => 'Aditional information for response option']); + $this->assertEquals($this->demographicResponse->getOptionsInputValue(), $expectedOptionsInputValue); + } } diff --git a/tests/demographicResponseOption/DAOTest.php b/tests/demographicResponseOption/DAOTest.php new file mode 100644 index 0000000..3619525 --- /dev/null +++ b/tests/demographicResponseOption/DAOTest.php @@ -0,0 +1,97 @@ +demographicResponseOptionDAO = app(DAO::class); + $this->addSchemaFile('demographicQuestion'); + $this->addSchemaFile('demographicResponseOption'); + $this->contextId = $this->createJournalMock(); + $this->demographicQuestionId = $this->createDemographicQuestion(); + } + + public function testNewDataObjectIsInstanceOfDemographicResponseOption(): void + { + $demographicResponseOption = $this->demographicResponseOptionDAO->newDataObject(); + self::assertInstanceOf(DemographicResponseOption::class, $demographicResponseOption); + } + + public function testCreateDemographicResponseOption(): void + { + $demographicResponseOption = $this->createDemographicResponseOptionObject(); + $insertedObjectId = $this->demographicResponseOptionDAO->insert($demographicResponseOption); + + $fetchedDemographicResponseOption = $this->demographicResponseOptionDAO->get( + $insertedObjectId, + $this->demographicQuestionId + ); + + self::assertEquals([ + 'id' => $insertedObjectId, + 'demographicQuestionId' => $this->demographicQuestionId, + 'optionText' => [self::DEFAULT_LOCALE => 'First response option, with input field'], + 'hasInputField' => true, + ], $fetchedDemographicResponseOption->getAllData()); + } + + public function testEditDemographicResponseOption(): void + { + $demographicResponseOption = $this->createDemographicResponseOptionObject(); + $insertedObjectId = $this->demographicResponseOptionDAO->insert($demographicResponseOption); + + $fetchedDemographicResponseOption = $this->demographicResponseOptionDAO->get( + $insertedObjectId, + $this->demographicQuestionId + ); + $fetchedDemographicResponseOption->setOptionText('Updated text', self::DEFAULT_LOCALE); + + $this->demographicResponseOptionDAO->update($fetchedDemographicResponseOption); + + $objectEdited = $this->demographicResponseOptionDAO->get( + $insertedObjectId, + $this->demographicQuestionId + ); + + self::assertEquals($objectEdited->getData('optionText'), [self::DEFAULT_LOCALE => 'Updated text']); + } + + public function testDeleteDemographicResponseOption(): void + { + $demographicResponseOption = $this->createDemographicResponseOptionObject(); + $insertedObjectId = $this->demographicResponseOptionDAO->insert($demographicResponseOption); + + $fetchedDemographicResponseOption = $this->demographicResponseOptionDAO->get( + $insertedObjectId, + $this->demographicQuestionId + ); + + $this->demographicResponseOptionDAO->delete($fetchedDemographicResponseOption); + self::assertFalse($this->demographicResponseOptionDAO->exists($insertedObjectId, $this->contextId)); + } +} diff --git a/tests/demographicResponseOption/DemographicResponseOptionTest.php b/tests/demographicResponseOption/DemographicResponseOptionTest.php new file mode 100644 index 0000000..34f34c0 --- /dev/null +++ b/tests/demographicResponseOption/DemographicResponseOptionTest.php @@ -0,0 +1,42 @@ +demographicResponseOption = new DemographicResponseOption(); + parent::setUp(); + } + + public function testGetDemographicQuestionId(): void + { + $expectedDemographicQuestionId = 1; + $this->demographicResponseOption->setDemographicQuestionId($expectedDemographicQuestionId); + + $this->assertEquals($expectedDemographicQuestionId, $this->demographicResponseOption->getDemographicQuestionId()); + } + + public function testGetResponseOptionText(): void + { + $expectedResponseOptionText = "Less than a minimum wage"; + $this->demographicResponseOption->setOptionText($expectedResponseOptionText, 'en'); + $optionText = $this->demographicResponseOption->getLocalizedOptionText(); + + $this->assertEquals($expectedResponseOptionText, $optionText); + } + + public function testGetHasInputField(): void + { + $hasInputField = true; + $this->demographicResponseOption->setHasInputField($hasInputField); + + $this->assertEquals($hasInputField, $this->demographicResponseOption->hasInputField()); + } +} diff --git a/tests/demographicResponseOption/RepositoryTest.php b/tests/demographicResponseOption/RepositoryTest.php new file mode 100644 index 0000000..282300e --- /dev/null +++ b/tests/demographicResponseOption/RepositoryTest.php @@ -0,0 +1,83 @@ +addSchemaFile('demographicQuestion'); + $this->addSchemaFile('demographicResponseOption'); + $this->demographicQuestionId = $this->createDemographicQuestion(); + $this->params = [ + 'demographicQuestionId' => $this->demographicQuestionId, + 'optionText' => [self::DEFAULT_LOCALE => 'First response option, with input field'], + 'hasInputField' => true, + ]; + } + + public function testGetNewDemographicResponseOptionObject(): void + { + $repository = app(Repository::class); + $responseOption = $repository->newDataObject(); + self::assertInstanceOf(DemographicResponseOption::class, $responseOption); + $responseOption = $repository->newDataObject($this->params); + self::assertEquals($this->params, $responseOption->_data); + } + + public function testResponseOptionCrud(): void + { + $repository = app(Repository::class); + $responseOption = $repository->newDataObject($this->params); + $insertedResponseOptionId = $repository->add($responseOption); + $this->params['id'] = $insertedResponseOptionId; + + $fetchedResponseOption = $repository->get($insertedResponseOptionId); + self::assertEquals($this->params, $fetchedResponseOption->getAllData()); + + $this->params['optionText']['en'] = 'Updated text'; + $repository->edit($responseOption, $this->params); + + $fetchedResponseOption = $repository->get($responseOption->getId()); + self::assertEquals($this->params, $fetchedResponseOption->getAllData()); + + $repository->delete($responseOption); + self::assertFalse($repository->exists($responseOption->getId())); + } + + public function testCollectorFilterByQuestion(): void + { + $repository = app(Repository::class); + $responseOption = $repository->newDataObject($this->params); + + $repository->add($responseOption); + + $responseOptions = $repository->getCollector() + ->filterByQuestionIds([$this->demographicQuestionId]) + ->getMany(); + + self::assertTrue(in_array($responseOption, $responseOptions->all())); + } +} diff --git a/tests/helpers/TestHelperTrait.php b/tests/helpers/TestHelperTrait.php index 1461f4a..a055259 100644 --- a/tests/helpers/TestHelperTrait.php +++ b/tests/helpers/TestHelperTrait.php @@ -22,9 +22,6 @@ private function createDemographicQuestion() 'questionType' => DemographicQuestion::TYPE_TEXTAREA, 'questionDescription' => [ self::DEFAULT_LOCALE => 'Test description' - ], - 'possibleResponses' => [ - self::DEFAULT_LOCALE => ['First possible response', 'Second possible response'] ] ]; @@ -33,11 +30,22 @@ private function createDemographicQuestion() return $repository->add($demographicQuestion); } + private function createDemographicResponseOptionObject() + { + $demographicResponseOption = $this->demographicResponseOptionDAO->newDataObject(); + $demographicResponseOption->setDemographicQuestionId($this->demographicQuestionId); + $demographicResponseOption->setOptionText('First response option, with input field', self::DEFAULT_LOCALE); + $demographicResponseOption->setHasInputField(true); + + return $demographicResponseOption; + } + private function createDemographicResponseObject($externalAuthor = false) { $demographicResponse = $this->demographicResponseDAO->newDataObject(); $demographicResponse->setDemographicQuestionId($this->demographicQuestionId); $demographicResponse->setValue([self::DEFAULT_LOCALE => 'Test text']); + $demographicResponse->setOptionsInputValue([45 => 'Aditional information for response option']); if ($externalAuthor) { $demographicResponse->setExternalId('external.author@lepidus.com.br'); diff --git a/version.xml b/version.xml index eb795ec..7061165 100644 --- a/version.xml +++ b/version.xml @@ -12,6 +12,6 @@ demographicData plugins.generic - 0.0.0.7 - 2024-09-26 + 0.0.0.8 + 2024-10-16