From 27234009350fe7ae48d307e3be6173a6a4814941 Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 29 Apr 2020 18:21:58 -0300 Subject: [PATCH 01/39] V4 laravel dreams --- app/Providers/AuthServiceProvider.php | 2 + bootstrap/lumen.php | 6 +- composer.json | 8 +- ...ll_export_batch_filename copy.php.ignoreme | 134 ++++++++++++++++++ src/Core/CoreConfig.php | 3 + src/Core/Traits/PrivateDeployment.php | 2 +- src/Init.php | 1 + tests/datasets/ushahidi/# Entity translations | 114 +++++++++++++++ tests/datasets/ushahidi/Base.yml | 4 + v4/Http/Controllers/README.md | 0 v4/Http/Controllers/SurveyController.php | 34 +++++ v4/Http/Controllers/V4Controller.php | 13 ++ v4/Http/README.md | 0 v4/Models/Attribute.php | 41 ++++++ v4/Models/Stage.php | 42 ++++++ v4/Models/Survey.php | 50 +++++++ v4/Policies/SurveyPolicy.php | 92 ++++++++++++ v4/README.md | 0 v4/routes/web.php | 20 +++ 19 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme create mode 100644 tests/datasets/ushahidi/# Entity translations create mode 100644 v4/Http/Controllers/README.md create mode 100644 v4/Http/Controllers/SurveyController.php create mode 100644 v4/Http/Controllers/V4Controller.php create mode 100644 v4/Http/README.md create mode 100644 v4/Models/Attribute.php create mode 100644 v4/Models/Stage.php create mode 100644 v4/Models/Survey.php create mode 100644 v4/Policies/SurveyPolicy.php create mode 100644 v4/README.md create mode 100644 v4/routes/web.php diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 300104bd18..93674d7117 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -59,6 +59,8 @@ public function boot() // Define passport scopes $this->defineScopes(); + // need to use a string here or laravel goes wild and doesn't authorize anything + Gate::policy('v4\Models\Survey', 'v4\Policies\SurveyPolicy'); } protected function defineScopes() diff --git a/bootstrap/lumen.php b/bootstrap/lumen.php index fd78586f22..06f38f4332 100644 --- a/bootstrap/lumen.php +++ b/bootstrap/lumen.php @@ -111,5 +111,9 @@ ], function ($router) { require __DIR__.'/../routes/web.php'; }); - +$app->router->group([ + 'namespace' => 'v4\Http\Controllers', +], function ($router) { + require __DIR__.'/../v4/routes/web.php'; +}); return $app; diff --git a/composer.json b/composer.json index 6ff3cb3951..186fc0a096 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,8 @@ "autoload": { "psr-4": { "Ushahidi\\": "src/", - "Ushahidi\\App\\": "app/" + "Ushahidi\\App\\": "app/", + "v4\\": "v4/" }, "files": [ "app/helpers.php" @@ -134,6 +135,11 @@ "phpspec run --no-code-generation", "behat --strict --profile ci" ], + "test-dev": [ + "phpunit --stop-on-failure", + "phpspec run", + "behat --strict" + ], "post-install-cmd": [ "\\SebastianFeldmann\\CaptainHook\\Composer\\Cmd::install", "php artisan passport:keys || php -r \"return 0;\"" diff --git a/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme b/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme new file mode 100644 index 0000000000..497901595a --- /dev/null +++ b/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme @@ -0,0 +1,134 @@ +table('translations') + ->addColumn('id') + ->addColumn('entity_type') //form, attribute,stage,category + ->addColumn('entity_id') + ->addColumn('translated_key')// ??? + ->addColumn('translation') + ->addColumn('language') + ->addTimestamps() + ->create(); + + // saving a form translation + + $table->insert([ + 'entity_type' => 'form', + 'entity_id' => 1, + 'translated_key' => 'name', + 'translation' => 'Nombre de la encuesta', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form', + 'entity_id' => 1, + 'translated_key' => 'description', + 'translation' => 'Descripcion de la encuesta', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => '1',//form_attribute_id for title + 'translated_key' => 'label', + 'translation' => 'El field title of the form translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 2,//form_attribute_id for description + 'translated_key' => 'label',//form_attributes.label + 'translation' => 'El field description translated', + 'language' => 'es-ES' + ]); + + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 2,//form_attribute_id for description + 'translated_key' => 'description',//form_attributes.description + 'translation' => 'The description for the description field :D', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 1,//form_attribute_id + 'translated_key' => 'label',//select + 'translation' => 'El field select translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 1,//form_attribute_id + 'translated_key' => 'description',//select + 'translation' => 'El field select translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 1,//form_attribute_id + 'translated_key' => 'default',//select + 'translation' => 'El field select translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_attributes', + 'entity_id' => 1,//form_attribute_id + 'translated_key' => 'options',//select + 'translation' => '["El field select translated", "Option 2"]', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_stages', + 'entity_id' => 4,//form_stage_id + 'translated_key' => 'name',//task name + 'translation' => 'El name del task translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'form_stages', + 'entity_id' => 4,//form_stage_id + 'translated_key' => 'description',//task name + 'translation' => 'El description del task translated', + 'language' => 'es-ES' + ]); + + + $table->insert([ + 'entity_type' => 'tags', + 'entity_id' => 46,//form_stage_id + 'translated_key' => 'name',//task name + 'translation' => 'El name del tag translated', + 'language' => 'es-ES' + ]); + + $table->insert([ + 'entity_type' => 'tags', + 'entity_id' => 46,//form_stage_id + 'translated_key' => 'description',//task name + 'translation' => 'El description del tag translated', + 'language' => 'es-ES' + ]); + + + } + + public function down() + { + // No op. Don't reverse this or it causes bugs + } +} diff --git a/src/Core/CoreConfig.php b/src/Core/CoreConfig.php index 190a468e43..c513ec6461 100644 --- a/src/Core/CoreConfig.php +++ b/src/Core/CoreConfig.php @@ -354,6 +354,9 @@ public function define(Container $di) $di->params['Ushahidi\Core\Tool\Authorizer\FormAuthorizer'] = [ 'form_repo' => $di->lazyGet('repository.form'), ]; + $di->params['v4\Policies\SurveyPolicy'] = [ + 'form_repo' => $di->lazyGet('repository.form'), + ]; $di->set('authorizer.form_attribute', $di->lazyNew('Ushahidi\Core\Tool\Authorizer\FormAttributeAuthorizer')); $di->params['Ushahidi\Core\Tool\Authorizer\FormAttributeAuthorizer'] = [ 'stage_repo' => $di->lazyGet('repository.form_stage'), diff --git a/src/Core/Traits/PrivateDeployment.php b/src/Core/Traits/PrivateDeployment.php index 51e93aa0c6..640f561dc3 100644 --- a/src/Core/Traits/PrivateDeployment.php +++ b/src/Core/Traits/PrivateDeployment.php @@ -39,7 +39,7 @@ public function isPrivate() * Check if user can access deployment * @return boolean */ - public function canAccessDeployment(User $user) + public function canAccessDeployment($user) { // Only logged in users have access if the deployment is private if ($this->isPrivate() and !$user->id) { diff --git a/src/Init.php b/src/Init.php index 69141a3b02..5907048a6e 100644 --- a/src/Init.php +++ b/src/Init.php @@ -38,6 +38,7 @@ function service($what = null) 'Ushahidi\App\Providers\LumenAuraConfig', 'Ushahidi\Console\ConsoleConfig', ]); + } if ($what) { return $di->get($what); diff --git a/tests/datasets/ushahidi/# Entity translations b/tests/datasets/ushahidi/# Entity translations new file mode 100644 index 0000000000..3dd4c567a9 --- /dev/null +++ b/tests/datasets/ushahidi/# Entity translations @@ -0,0 +1,114 @@ +# Entity translations + + +### Site wide configuration for available languages + +Add the enabled_languages key and object + +PUT http://192.168.33.110/api/v3/config/site +``` +{ + "id": "site", + "url": "http://192.168.33.110/api/v3/config/site", + "enabled_languages": { + "default": "en-US", + "available": ["es-ES", "pt-BR"] + }, + "name": "Deployer", + "description": "Hello!", + "email": "", + "timezone": "UTC", + "language": "en-US", + "date_format": "n/j/Y", + "client_url": false, + "first_login": true, + "tier": "free", + "private": false, + "allowed_privileges": [ + "read", + "search" + ] +} +``` + +## Sending a new survey with language +Use the default /forms endpoint, add support for enabled_languages. +The enabled_languages key will be required, with "default" being "en-EN" and "available" being an empty array on creation. +We will run a migration to add this field and value to all current surveys. +When we release, we will let users know of this feature through intercom so that they can go and ajust the default language manually. + +POST http://192.168.33.110/api/v3/forms + +``` +{ + "enabled_languages": { + "default": "en-EN", + "available" : [] + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "tasks": [ + { + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "attributes": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {}, + "form_stage_id": "interim_id_0" + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {}, + "form_stage_id": "interim_id_1" + } + ], + "is_public": true, + "id": "interim_id_2" + } + ], + "name": "Hello world", + "description": "A survey that I plan on translating" +} +``` +PUT http://192.168.33.110/api/v3/forms/{id}/translate + +Receives the translated attribute fields, stages, form fields (desc/title) and the language it's being translated to. + +{ + "form_fields": { + "name": "Hola mundo", + "description": "Una encuesta que planeo a traducir." + }, + "stages": [ + { + "id": 1, + "attributes": { + "{key}": { + "label": "Una etiqueta", //name + "instructions": "Instrucciones para el campo" //description + } + }, + "description": "La descripcion del stage", + "label": "El nombre del stage" + } + ] + "language": "es-ES" +} diff --git a/tests/datasets/ushahidi/Base.yml b/tests/datasets/ushahidi/Base.yml index 21d3f62338..6d4661e029 100644 --- a/tests/datasets/ushahidi/Base.yml +++ b/tests/datasets/ushahidi/Base.yml @@ -1826,6 +1826,10 @@ config: group_name: features config_key: hxl config_value: '{"enabled":true}' + - + group_name: site + config_key: enabled_languages + config_value: '{default: "en-US", available: ["es-ES", "pt-BR"]}' contacts: - id: 1 diff --git a/v4/Http/Controllers/README.md b/v4/Http/Controllers/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php new file mode 100644 index 0000000000..290fb9a7c2 --- /dev/null +++ b/v4/Http/Controllers/SurveyController.php @@ -0,0 +1,34 @@ +authorize('index', Survey::class); + return Survey::all(); + } +} diff --git a/v4/Http/Controllers/V4Controller.php b/v4/Http/Controllers/V4Controller.php new file mode 100644 index 0000000000..1c9091b37d --- /dev/null +++ b/v4/Http/Controllers/V4Controller.php @@ -0,0 +1,13 @@ +belongsTo('v4\Models\Stage', 'form_stage_id'); + } + +} diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php new file mode 100644 index 0000000000..737eba0658 --- /dev/null +++ b/v4/Models/Stage.php @@ -0,0 +1,42 @@ +hasMany('v4\Models\Attribute', 'form_stage_id'); + } + + public function survey() { + return $this->belongsTo('v4\Models\Survey', 'form_id'); + } + +} diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php new file mode 100644 index 0000000000..b715089ae6 --- /dev/null +++ b/v4/Models/Survey.php @@ -0,0 +1,50 @@ +hasMany('v4\Models\Stage', 'form_id'); + } + +} diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php new file mode 100644 index 0000000000..03e16a254c --- /dev/null +++ b/v4/Policies/SurveyPolicy.php @@ -0,0 +1,92 @@ +user = $user; + $this->isAllowed(null, 'read'); + return false; + } + public function isAllowed($entity, $privilege){ + $authorizer = service('authorizer.form'); + + // These checks are run within the user context. + $user = $this->user; + // Only logged in users have access if the deployment is private + if (!$this->canAccessDeployment($user)) { + return false; + } + + // Allow role with the right permissions + if ($this->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { + return true; + } + + if ($this->isUserAdmin($user)) { + return true; + } + + // We check if the user has access to a parent form. This check has to be run + // before public access is granted! + // @CHECK: what is a parent form????? + // if (!$this->isAllowedParent($entity, $privilege, $user)) { + // return false; + // } + + // If a form is not disabled, then *anyone* can view it. + // @TODO how to do this for a index policy? + // if ($privilege === 'read' && !$this->isFormDisabled($entity)) { + // return true; + // } + + + // All users are allowed to search forms. + // @TODO should only do 'search' here. Do 'read' above in the isFormDisabled check + if ($privilege === 'search' || $privilege === 'read') { + return true; + } + + } + protected function getParent(Entity $entity){} +} \ No newline at end of file diff --git a/v4/README.md b/v4/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/v4/routes/web.php b/v4/routes/web.php new file mode 100644 index 0000000000..bfe15eca7b --- /dev/null +++ b/v4/routes/web.php @@ -0,0 +1,20 @@ +group([ + 'prefix' => $apiBase, +], function () use ($router) { + // Forms + $router->group([ + // 'namespace' => 'Forms', + 'prefix' => 'surveys', + // 'middleware' => ['scope:forms', 'expiration'] + ], function () use ($router) { + // Public access + $router->get('/', 'SurveyController@index'); + }); +}); From 8381626f08b86855ba779e4b6261ca90f19df09f Mon Sep 17 00:00:00 2001 From: rowasc Date: Mon, 4 May 2020 20:12:49 +0000 Subject: [PATCH 02/39] Add surveys endpoint with authorizers to v4/surveys --- src/Core/Tool/Permissions/AclTrait.php | 2 +- v4/Policies/SurveyPolicy.php | 33 ++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Core/Tool/Permissions/AclTrait.php b/src/Core/Tool/Permissions/AclTrait.php index 61952e3f0a..709f02dc8f 100644 --- a/src/Core/Tool/Permissions/AclTrait.php +++ b/src/Core/Tool/Permissions/AclTrait.php @@ -15,7 +15,7 @@ trait AclTrait { - protected $acl; + public $acl; public function setAcl(Acl $acl) { diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index 03e16a254c..e5aa63b295 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -33,10 +33,12 @@ class SurveyPolicy // Check that the user has the necessary permissions use AclTrait; + protected $user; // It requires a `FormRepository` to load parent posts too. protected $form_repo; + /** * * @param \App\User $user @@ -45,21 +47,22 @@ class SurveyPolicy public function index(User $user) { $this->user = $user; - $this->isAllowed(null, 'read'); - return false; + return $this->isAllowed(null, 'read'); } + public function isAllowed($entity, $privilege){ $authorizer = service('authorizer.form'); - + // These checks are run within the user context. - $user = $this->user; + $user = $authorizer->getUser(); + // Only logged in users have access if the deployment is private if (!$this->canAccessDeployment($user)) { return false; } // Allow role with the right permissions - if ($this->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { + if ($authorizer->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { return true; } @@ -67,26 +70,26 @@ public function isAllowed($entity, $privilege){ return true; } - // We check if the user has access to a parent form. This check has to be run - // before public access is granted! - // @CHECK: what is a parent form????? + // Before /v4 we would check if the user has access to a parent form. This check has to be run + // before public access is granted... but parent forms aren't a thing + // @IMPORTANT : parent forms are not a thing, they don't do anything, they don't exist. + // I leave this here because it can be confusing otherwise. // if (!$this->isAllowedParent($entity, $privilege, $user)) { // return false; // } // If a form is not disabled, then *anyone* can view it. - // @TODO how to do this for a index policy? - // if ($privilege === 'read' && !$this->isFormDisabled($entity)) { - // return true; - // } - + if ($privilege === 'read' && !$this->isFormDisabled($entity)) { + return true; + } // All users are allowed to search forms. // @TODO should only do 'search' here. Do 'read' above in the isFormDisabled check - if ($privilege === 'search' || $privilege === 'read') { + if ($privilege === 'search') { return true; } + return false; } protected function getParent(Entity $entity){} -} \ No newline at end of file +} From 5caefc01b210bd2b0a4b541e74c40874354cb190 Mon Sep 17 00:00:00 2001 From: rowasc Date: Mon, 4 May 2020 20:54:35 +0000 Subject: [PATCH 03/39] Testing the acl setup with v4 api --- tests/datasets/ushahidi/Base.yml | 39 +++++++++++++++++-- tests/integration/acl.v4.feature | 31 +++++++++++++++ .../bootstrap/PHPUnitFixtureContext.php | 2 +- 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 tests/integration/acl.v4.feature diff --git a/tests/datasets/ushahidi/Base.yml b/tests/datasets/ushahidi/Base.yml index 6d4661e029..f51f328793 100644 --- a/tests/datasets/ushahidi/Base.yml +++ b/tests/datasets/ushahidi/Base.yml @@ -233,6 +233,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "post" + priority: 1 - id: 2 form_id: 1 @@ -240,6 +241,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "task" + priority: 2 - id: 3 form_id: 1 @@ -247,6 +249,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "task" + priority: 3 - id: 4 form_id: 2 @@ -254,6 +257,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "post" + priority: 1 - id: 5 form_id: 3 @@ -261,6 +265,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "post" + priority: 1 - id: 6 form_id: 4 @@ -268,6 +273,7 @@ form_stages: show_when_published: 0 task_is_internal_only: 0 type: "task" + priority: 1 - id: 7 form_id: 4 @@ -275,6 +281,7 @@ form_stages: show_when_published: 1 task_is_internal_only: 0 type: "post" + priority: 2 - id: 8 form_id: 5 @@ -282,6 +289,7 @@ form_stages: show_when_published: 0 task_is_internal_only: 0 type: "post" + priority: 1 - id: 9 form_id: 7 @@ -289,6 +297,7 @@ form_stages: show_when_published: 0 task_is_internal_only: 0 type: "post" + priority: 1 - id: 10 form_id: 6 @@ -296,6 +305,7 @@ form_stages: show_when_published: 0 task_is_internal_only: 0 type: "post" + priority: 1 - id: 11 form_id: 8 @@ -303,6 +313,31 @@ form_stages: show_when_published: 0 task_is_internal_only: 0 type: "post" + priority: 1 + - + id: 12 + form_id: 8 + label: "Post" + show_when_published: 0 + task_is_internal_only: 1 + type: "task" + priority: 2 + - + id: 13 + form_id: 8 + label: "Post" + show_when_published: 0 + task_is_internal_only: 0 + type: "task" + priority: 3 + - + id: 14 + form_id: 8 + label: "Public task" + show_when_published: 1 + task_is_internal_only: 0 + type: "post" + priority: 4 form_attributes: - id: 1 @@ -1826,10 +1861,6 @@ config: group_name: features config_key: hxl config_value: '{"enabled":true}' - - - group_name: site - config_key: enabled_languages - config_value: '{default: "en-US", available: ["es-ES", "pt-BR"]}' contacts: - id: 1 diff --git a/tests/integration/acl.v4.feature b/tests/integration/acl.v4.feature new file mode 100644 index 0000000000..73c25def88 --- /dev/null +++ b/tests/integration/acl.v4.feature @@ -0,0 +1,31 @@ +@acl +Feature: V4 API Access Control Layer + Scenario: Listing All Stages for a form with hidden stages + Given that I want to get all "Surveys" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.7.stages" property + And the "results.7.stages" property count is "1" + And the "results.7.stages.0.attributes" property count is "0" + Then the guzzle status code should be 200 + Scenario: Listing All attributes for a stage in a multi-stage form + Given that I want to get all "Surveys" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.0.stages" property + And the "results.0.stages" property count is "3" + And the "results.0.stages.0.attributes" property count is "17" + Then the guzzle status code should be 200 + Scenario: Listing All Stages and attributes for a form with hidden stages as admin + Given that I want to get all "Surveys" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.7.stages" property + And the "results.7.stages" property count is "4" + And the "results.7.stages.0.attributes" property count is "2" diff --git a/tests/integration/bootstrap/PHPUnitFixtureContext.php b/tests/integration/bootstrap/PHPUnitFixtureContext.php index e0d3eb5489..f4e0033403 100644 --- a/tests/integration/bootstrap/PHPUnitFixtureContext.php +++ b/tests/integration/bootstrap/PHPUnitFixtureContext.php @@ -1,4 +1,4 @@ - Date: Mon, 4 May 2020 20:57:55 +0000 Subject: [PATCH 04/39] Add normal scopes to v4 apis (from Forms v3 endpoint) --- v4/routes/web.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v4/routes/web.php b/v4/routes/web.php index bfe15eca7b..6b63c663a7 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -12,7 +12,7 @@ $router->group([ // 'namespace' => 'Forms', 'prefix' => 'surveys', - // 'middleware' => ['scope:forms', 'expiration'] + 'middleware' => ['scope:forms', 'expiration'] ], function () use ($router) { // Public access $router->get('/', 'SurveyController@index'); From b51228d4038d6ba1cbe2929107b03a5a4b11e1e8 Mon Sep 17 00:00:00 2001 From: rowasc Date: Mon, 4 May 2020 21:17:46 +0000 Subject: [PATCH 05/39] v4: wrap results in a [results] array key to behave like v3 --- v4/Http/Controllers/SurveyController.php | 6 +++--- v4/Policies/SurveyPolicy.php | 2 +- v4/routes/web.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 290fb9a7c2..8b20631cf3 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -24,11 +24,11 @@ public function show(Survey $survey) * Display the specified resource. * * @param \Modules\Block $block - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse */ public function index() - { + { $this->authorize('index', Survey::class); - return Survey::all(); + return response()->json(['results' => Survey::all()]); } } diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index e5aa63b295..009eb655bc 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -47,7 +47,7 @@ class SurveyPolicy public function index(User $user) { $this->user = $user; - return $this->isAllowed(null, 'read'); + return $this->isAllowed(null, 'search'); } public function isAllowed($entity, $privilege){ diff --git a/v4/routes/web.php b/v4/routes/web.php index 6b63c663a7..dcb2a0c6b2 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -12,7 +12,7 @@ $router->group([ // 'namespace' => 'Forms', 'prefix' => 'surveys', - 'middleware' => ['scope:forms', 'expiration'] + 'middleware' => ['scope:forms', 'expiration'] ], function () use ($router) { // Public access $router->get('/', 'SurveyController@index'); From f9559cb45061a1edd82a173f660bee3bce2521ab Mon Sep 17 00:00:00 2001 From: rowasc Date: Tue, 5 May 2020 00:43:50 +0000 Subject: [PATCH 06/39] v4 namespace tests (with lumen) now passing. Stages are hydrated only when the user has the right perms. --- src/App/Acl.php | 1 + tests/integration/acl.v4.feature | 2 +- v4/Models/Stage.php | 1 + v4/Models/Survey.php | 22 ++++++++++++++++-- v4/Policies/SurveyPolicy.php | 40 +++++++++++++++++++++++++------- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/App/Acl.php b/src/App/Acl.php index c95319c528..3df8a6a056 100644 --- a/src/App/Acl.php +++ b/src/App/Acl.php @@ -52,6 +52,7 @@ public function hasPermission(User $user, $permission) } } + protected function customRoleHasPermission(User $user, $permission) { $role = $this->role_repo->getByName($user->role); diff --git a/tests/integration/acl.v4.feature b/tests/integration/acl.v4.feature index 73c25def88..0b75de02e7 100644 --- a/tests/integration/acl.v4.feature +++ b/tests/integration/acl.v4.feature @@ -20,7 +20,7 @@ Feature: V4 API Access Control Layer And the "results.0.stages" property count is "3" And the "results.0.stages.0.attributes" property count is "17" Then the guzzle status code should be 200 - Scenario: Listing All Stages and attributes for a form with hidden stages as admin + Scenario: Listing All Stages for a form with hidden stages as admin Given that I want to get all "Surveys" And that the oauth token is "testadminuser" And that the api_url is "api/v4" diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index 737eba0658..5765d125f9 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -3,6 +3,7 @@ namespace v4\Models; use Illuminate\Database\Eloquent\Model; +use Ushahidi\Core\Entity\Permission; class Stage extends Model { diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index b715089ae6..30fd7cca24 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -3,15 +3,19 @@ namespace v4\Models; use Illuminate\Database\Eloquent\Model; +use Ushahidi\Core\Entity\Permission; +use Ushahidi\Core\Tool\Permissions\InteractsWithFormPermissions; + class Survey extends Model { + use InteractsWithFormPermissions; protected $table = 'forms'; protected $with = ['stages']; /** * The attributes that should be hidden for serialization. * @note this should be changed so that we either use the fractal transformer - * OR a policy authorizer which is a more or less accepted method to do it + * OR a policy authorizer which is a more or less accepted method to do it * (which uses the same $hidden type thing but it's much nicer obviously) * * @var array @@ -42,9 +46,23 @@ class Survey extends Model 'targeted_survey' ]; + /** + * We check for relationship permissions here, to avoid hydrating anything that should not be hydrated. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function stages() { - return $this->hasMany('v4\Models\Stage', 'form_id'); + $authorizer = service('authorizer.form'); + $user = $authorizer->getUser(); + // NOTE: this acl->hasPermission check is all `canUserEditForm` does, so we're doing that directly + // to avoid an hydration issue with InteractsWithFormPermissions + if ($authorizer->acl->hasPermission($user, Permission::MANAGE_POSTS)) { + // if this permission is set we can go ahead and hydrate all the stages + return $this->hasMany('v4\Models\Stage', 'form_id'); + } + return $this->hasMany('v4\Models\Stage', 'form_id') + ->where('form_stages.show_when_published', '=', '1') + ->where('form_stages.task_is_internal_only', '=', '0'); } } diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index 009eb655bc..3d3be46add 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -2,11 +2,9 @@ namespace v4\Policies; +use Ushahidi\Core\Entity; use v4\Models\Survey; -use Ushahidi\App\Auth\GenericUser as User; - use Ushahidi\Core\Entity\Permission; -use Ushahidi\Core\Tool\Authorizer; use Ushahidi\Core\Traits\AdminAccess; use Ushahidi\Core\Traits\UserContext; use Ushahidi\Core\Traits\ParentAccess; @@ -21,9 +19,8 @@ class SurveyPolicy use UserContext; // It uses methods from several traits to check access: - // - `ParentAccess` to check if the user can access the parent, // - `AdminAccess` to check if the user has admin access - use AdminAccess, ParentAccess; + use AdminAccess; // It uses `PrivAccess` to provide the `getAllowedPrivs` method. use PrivAccess; @@ -44,12 +41,27 @@ class SurveyPolicy * @param \App\User $user * @return bool */ - public function index(User $user) + public function index() { - $this->user = $user; - return $this->isAllowed(null, 'search'); + $empty_form = new Entity\Form(); + return $this->isAllowed($empty_form, 'search'); + } + + /** + * @param Survey $survey + * @return bool + */ + public function update(Survey $survey) { + // we convert to a form entity to be able to continue using the old authorizers and classes. + $form = new Entity\Form($survey->toArray()); + return $this->isAllowed($form, 'update'); } + /** + * @param $entity + * @param string $privilege + * @return bool + */ public function isAllowed($entity, $privilege){ $authorizer = service('authorizer.form'); @@ -91,5 +103,15 @@ public function isAllowed($entity, $privilege){ return false; } - protected function getParent(Entity $entity){} + + /** + * Check if a form is disabled. + * @param Entity $entity + * @return Boolean + */ + protected function isFormDisabled(Entity\Form $entity) + { + return (bool) $entity->disabled; + } + } From 8e712283bd5c8fcf376634e152ad91265dd8554d Mon Sep 17 00:00:00 2001 From: rowasc Date: Tue, 5 May 2020 01:10:02 +0000 Subject: [PATCH 07/39] v4 namespace now allows you to get a single survey. Tests confirm stage permissions are correct --- tests/integration/acl.v4.feature | 25 +++++++++++++++++++++--- v4/Http/Controllers/SurveyController.php | 13 +++++++----- v4/Policies/SurveyPolicy.php | 15 +++++++++++++- v4/routes/web.php | 1 + 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/integration/acl.v4.feature b/tests/integration/acl.v4.feature index 0b75de02e7..b29a33e09c 100644 --- a/tests/integration/acl.v4.feature +++ b/tests/integration/acl.v4.feature @@ -1,6 +1,6 @@ @acl Feature: V4 API Access Control Layer - Scenario: Listing All Stages for a form with hidden stages + Scenario: Listing All Stages for all forms with hidden stages Given that I want to get all "Surveys" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" @@ -10,7 +10,7 @@ Feature: V4 API Access Control Layer And the "results.7.stages" property count is "1" And the "results.7.stages.0.attributes" property count is "0" Then the guzzle status code should be 200 - Scenario: Listing All attributes for a stage in a multi-stage form + Scenario: Listing All attributes for a stage in an array of forms with a multi-stage form Given that I want to get all "Surveys" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" @@ -20,7 +20,7 @@ Feature: V4 API Access Control Layer And the "results.0.stages" property count is "3" And the "results.0.stages.0.attributes" property count is "17" Then the guzzle status code should be 200 - Scenario: Listing All Stages for a form with hidden stages as admin + Scenario: Listing All Stages for a form in an array of forms with hidden stages as admin Given that I want to get all "Surveys" And that the oauth token is "testadminuser" And that the api_url is "api/v4" @@ -29,3 +29,22 @@ Feature: V4 API Access Control Layer And the response has a "results.7.stages" property And the "results.7.stages" property count is "4" And the "results.7.stages.0.attributes" property count is "2" + Scenario: Listing All Stages for a form with hidden stages as admin + Given that I want to find a "Survey" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + When I request "/surveys/8" + Then the response is JSON + And the response has a "survey.stages" property + And the "survey.stages" property count is "4" + And the "survey.stages.0.attributes" property count is "2" + And the "survey.stages.2.attributes" property count is "0" + Scenario: Listing All Stages for a form with hidden stages as a normal user + Given that I want to find a "Survey" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys/8" + Then the response is JSON + And the response has a "survey.stages" property + And the "survey.stages" property count is "1" + And the "survey.stages.0.attributes" property count is "0" diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 8b20631cf3..b7a59a8c49 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -12,19 +12,22 @@ class SurveyController extends V4Controller /** * Display the specified resource. * - * @param \Modules\Block $block - * @return \Illuminate\Http\Response + * @param int $id + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function show(Survey $survey) + public function show(int $id) { - // + $survey = Survey::find($id); + $this->authorize('show', $survey); + return response()->json(['survey' => $survey]); } /** * Display the specified resource. * - * @param \Modules\Block $block * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException */ public function index() { diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index 3d3be46add..6fa0ab39c0 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -1,7 +1,8 @@ isAllowed($empty_form, 'search'); } + /** + * + * @param GenericUser $user + * @param Survey $survey + * @return bool + */ + public function show(User $user, Survey $survey) + { + $form = new Entity\Form($survey->toArray()); + return $this->isAllowed($form, 'read'); + } + /** * @param Survey $survey * @return bool diff --git a/v4/routes/web.php b/v4/routes/web.php index dcb2a0c6b2..b1dfc75811 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -16,5 +16,6 @@ ], function () use ($router) { // Public access $router->get('/', 'SurveyController@index'); + $router->get('/{id}', 'SurveyController@show'); }); }); From 0643fe1a4c87820520c0c391d14d61cd8663d95b Mon Sep 17 00:00:00 2001 From: rowasc Date: Tue, 5 May 2020 02:42:00 +0000 Subject: [PATCH 08/39] Add survey save feature - beware: missing tests ! --- v4/Http/Controllers/SurveyController.php | 145 +++++++++++++++++++++++ v4/Models/Attribute.php | 29 +++-- v4/Models/Stage.php | 2 + v4/Models/Survey.php | 2 +- v4/routes/web.php | 8 ++ 5 files changed, 173 insertions(+), 13 deletions(-) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index b7a59a8c49..1b774adc06 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -1,10 +1,14 @@ authorize('index', Survey::class); return response()->json(['results' => Survey::all()]); } + + public function store(Request $request) { + $validator = $this->getValidationFactory()->make($request->input(), [ + 'name' => [ + 'required', + 'min:2', + 'max:255', + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT + ], + 'description' => [ + 'string', + 'nullable' + ], + //@TODO find out where this color validator is implemented + //[['color']], + 'color' => [ + 'string', + 'nullable' + ], + 'disabled' => [ + 'boolean' + ], + 'hide_author' => [ + 'boolean' + ], + 'hide_location' => [ + 'boolean' + ], + 'hide_time' => [ + 'boolean' + ], + // @FIXME: disabled targeted survey creation for v4 forms, need to check + 'targeted_survey' => [ + Rule::in([false]), + ], + 'stages.*.label' => [ + 'required', + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT + ], + 'stages.*.type' => [ + Rule::in(['post', 'task']) + ], + 'stages.*.priority' => [ + 'numeric', + ], + 'stages.*.icon' => [ + 'alpha', + ], + 'stages.*.attributes.*.label' => [ + 'required', + 'max:150' + ], + 'stages.*.attributes.*.key' => [ + 'max:150', + 'alpha_dash' + // @TODO: add this validation for keys + //[[$this->repo, 'isKeyAvailable'], [':value']] + ], + 'stages.*.attributes.*.input' => [ + 'required', + Rule::in([ + 'text', + 'textarea', + 'select', + 'radio', + 'checkbox', + 'checkboxes', + 'date', + 'datetime', + 'location', + 'number', + 'relation', + 'upload', + 'video', + 'markdown', + 'tags', + ]) + ], + 'stages.*.attributes.*.type' => [ + 'required', + Rule::in([ + 'decimal', + 'int', + 'geometry', + 'text', + 'varchar', + 'markdown', + 'point', + 'datetime', + 'link', + 'relation', + 'media', + 'title', + 'description', + 'tags', + ]) + // @TODO: add this validation for duplicates in type? + //[[$this, 'checkForDuplicates'], [':validation', ':value']], + ], + 'stages.*.attributes.*.type' => [ + 'boolean' + ], + 'stages.*.attributes.*.priority' => [ + 'numeric', + ], + 'stages.*.attributes.*.cardinality' => [ + 'numeric', + ], + 'stages.*.attributes.*.response_private' => [ + 'boolean' + // @TODO add this custom validator for canMakePrivate + // [[$this, 'canMakePrivate'], [':value', $type]] + ] + // @NOTE: checkPostTypeLimit is not used here. + // Before merge, validate with Angela if we + // should be removing that arbitrary limit since it's pretty rare + // for it to be needed + ]); + $survey = Survey::create( + array_merge( + $request->input(),[ 'updated' => time(), 'created' => time()] + ) + ); + foreach ($request->input('stages') as $stage) { + $stage_model = $survey->stages()->create( + array_merge( + $stage, [ 'updated' => time(), 'created' => time()] + ) + ); + foreach ($stage['attributes'] as $attribute) { + $uuid = Uuid::uuid4(); + $attribute['key'] = $uuid->toString(); + $stage_model->attributes()->create( + array_merge( + $attribute, [ 'updated' => time(), 'created' => time()] + ) + ); + } + } + return response()->json(['survey' => $survey->load('stages')]); + } } diff --git a/v4/Models/Attribute.php b/v4/Models/Attribute.php index 6434336543..d5b396136d 100644 --- a/v4/Models/Attribute.php +++ b/v4/Models/Attribute.php @@ -6,6 +6,7 @@ class Attribute extends Model { + public $timestamps = FALSE; protected $table = 'form_attributes'; /** @@ -20,20 +21,24 @@ class Attribute extends Model * @var array */ protected $fillable = [ - 'parent_id', - 'name', - 'description', + 'key', + 'label', + 'instructions', + 'input', 'type', - 'disabled', - 'require_approval', - 'everyone_can_create', - 'color', - 'hide_author', - 'hide_time', - 'hide_location', - 'targeted_survey' + 'required', + 'default', + 'priority', + 'options', + 'cardinality', + 'config', + 'response_private', + 'form_stage_id' + ]; + protected $casts = [ + 'config' => 'json', + 'options' => 'json', ]; - public function stage () { return $this->belongsTo('v4\Models\Stage', 'form_stage_id'); } diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index 5765d125f9..f8e3306bc0 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -7,6 +7,8 @@ class Stage extends Model { + public $timestamps = FALSE; + protected $table = 'form_stages'; /** * The attributes that should be mutated to dates. diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 30fd7cca24..b377827dda 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -11,7 +11,7 @@ class Survey extends Model use InteractsWithFormPermissions; protected $table = 'forms'; protected $with = ['stages']; - + public $timestamps = FALSE; /** * The attributes that should be hidden for serialization. * @note this should be changed so that we either use the fractal transformer diff --git a/v4/routes/web.php b/v4/routes/web.php index b1dfc75811..1c86136d09 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -18,4 +18,12 @@ $router->get('/', 'SurveyController@index'); $router->get('/{id}', 'SurveyController@show'); }); + + // Restricted access + $router->group([ + 'prefix' => 'surveys', + 'middleware' => ['auth:api', 'scope:forms'] + ], function () use ($router) { + $router->post('/', 'SurveyController@store'); + }); }); From 125d4511e0b0288237ac8d82c4fd9145675d3ce3 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 5 May 2020 17:29:27 -0300 Subject: [PATCH 09/39] Add create tests to v4/survey endpoint and add policy for it --- tests/integration/acl.v4.feature | 50 ------- tests/integration/v4/acl.v4.feature | 165 +++++++++++++++++++++++ v4/Http/Controllers/SurveyController.php | 1 + v4/Policies/SurveyPolicy.php | 10 ++ 4 files changed, 176 insertions(+), 50 deletions(-) delete mode 100644 tests/integration/acl.v4.feature create mode 100644 tests/integration/v4/acl.v4.feature diff --git a/tests/integration/acl.v4.feature b/tests/integration/acl.v4.feature deleted file mode 100644 index b29a33e09c..0000000000 --- a/tests/integration/acl.v4.feature +++ /dev/null @@ -1,50 +0,0 @@ -@acl -Feature: V4 API Access Control Layer - Scenario: Listing All Stages for all forms with hidden stages - Given that I want to get all "Surveys" - And that the oauth token is "testbasicuser" - And that the api_url is "api/v4" - When I request "/surveys" - Then the response is JSON - And the response has a "results.7.stages" property - And the "results.7.stages" property count is "1" - And the "results.7.stages.0.attributes" property count is "0" - Then the guzzle status code should be 200 - Scenario: Listing All attributes for a stage in an array of forms with a multi-stage form - Given that I want to get all "Surveys" - And that the oauth token is "testbasicuser" - And that the api_url is "api/v4" - When I request "/surveys" - Then the response is JSON - And the response has a "results.0.stages" property - And the "results.0.stages" property count is "3" - And the "results.0.stages.0.attributes" property count is "17" - Then the guzzle status code should be 200 - Scenario: Listing All Stages for a form in an array of forms with hidden stages as admin - Given that I want to get all "Surveys" - And that the oauth token is "testadminuser" - And that the api_url is "api/v4" - When I request "/surveys" - Then the response is JSON - And the response has a "results.7.stages" property - And the "results.7.stages" property count is "4" - And the "results.7.stages.0.attributes" property count is "2" - Scenario: Listing All Stages for a form with hidden stages as admin - Given that I want to find a "Survey" - And that the oauth token is "testadminuser" - And that the api_url is "api/v4" - When I request "/surveys/8" - Then the response is JSON - And the response has a "survey.stages" property - And the "survey.stages" property count is "4" - And the "survey.stages.0.attributes" property count is "2" - And the "survey.stages.2.attributes" property count is "0" - Scenario: Listing All Stages for a form with hidden stages as a normal user - Given that I want to find a "Survey" - And that the oauth token is "testbasicuser" - And that the api_url is "api/v4" - When I request "/surveys/8" - Then the response is JSON - And the response has a "survey.stages" property - And the "survey.stages" property count is "1" - And the "survey.stages.0.attributes" property count is "0" diff --git a/tests/integration/v4/acl.v4.feature b/tests/integration/v4/acl.v4.feature new file mode 100644 index 0000000000..4214173466 --- /dev/null +++ b/tests/integration/v4/acl.v4.feature @@ -0,0 +1,165 @@ +@acl +Feature: V4 API Access Control Layer + Scenario: Listing All Stages for all forms with hidden stages + Given that I want to get all "Surveys" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.7.stages" property + And the "results.7.stages" property count is "1" + And the "results.7.stages.0.attributes" property count is "0" + Then the guzzle status code should be 200 + Scenario: Listing All attributes for a stage in an array of forms with a multi-stage form + Given that I want to get all "Surveys" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.0.stages" property + And the "results.0.stages" property count is "3" + And the "results.0.stages.0.attributes" property count is "17" + Then the guzzle status code should be 200 + Scenario: Listing All Stages for a form in an array of forms with hidden stages as admin + Given that I want to get all "Surveys" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "results.7.stages" property + And the "results.7.stages" property count is "4" + And the "results.7.stages.0.attributes" property count is "2" + Scenario: Listing All Stages for a form with hidden stages as admin + Given that I want to find a "Survey" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + When I request "/surveys/8" + Then the response is JSON + And the response has a "survey.stages" property + And the "survey.stages" property count is "4" + And the "survey.stages.0.attributes" property count is "2" + And the "survey.stages.2.attributes" property count is "0" + Scenario: Listing All Stages for a form with hidden stages as a normal user + Given that I want to find a "Survey" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/surveys/8" + Then the response is JSON + And the response has a "survey.stages" property + And the "survey.stages" property count is "1" + And the "survey.stages.0.attributes" property count is "0" + @rolesEnabled + Scenario: User with Manage Settings permission can create a hydrated form + Given that I want to make a new "Survey" + And that the oauth token is "testmanager" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "enabled_languages": { + "default": "en-EN" + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + "stages": [ + { + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "attributes": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {} + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {} + } + ], + "is_public": true + } + ], + "name": "new" + } + """ + When I request "/surveys" + Then the response is JSON + And the response has a "survey" property + And the response has a "survey.id" property + And the type of the "survey.id" property is "numeric" + And the response has a "survey.stages" property + And the type of the "survey.stages" property is "array" + And the response has a "survey.stages.0.attributes" property + And the "survey.stages.0.attributes" property count is "2" + And the "survey.name" property equals "new" + Then the guzzle status code should be 200 + @rolesEnabled + Scenario: Basic user CANNOT create a hydrated form + Given that I want to make a new "Survey" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "enabled_languages": { + "default": "en-EN" + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + "stages": [ + { + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "attributes": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {} + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {} + } + ], + "is_public": true + } + ], + "name": "new" + } + """ + When I request "/surveys" + Then the guzzle status code should be 403 diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 1b774adc06..28c514a6ba 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -40,6 +40,7 @@ public function index() } public function store(Request $request) { + $this->authorize('store', Survey::class); $validator = $this->getValidationFactory()->make($request->input(), [ 'name' => [ 'required', diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index 6fa0ab39c0..295714fd41 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -70,6 +70,16 @@ public function update(Survey $survey) { return $this->isAllowed($form, 'update'); } + + /** + * @param Survey $survey + * @return bool + */ + public function store() { + // we convert to a form entity to be able to continue using the old authorizers and classes. + $form = new Entity\Form(); + return $this->isAllowed($form, 'create'); + } /** * @param $entity * @param string $privilege From 36124da2bc5a6330c7c9c391ef8d8bcb170cbd62 Mon Sep 17 00:00:00 2001 From: rowasc Date: Wed, 6 May 2020 22:56:42 +0000 Subject: [PATCH 10/39] Entity Translations: load the translations into entities, seamlessly --- bootstrap/lumen.php | 1 + ...20200506131856_add_entity_translations.php | 19 ++ tests/integration/v4/acl.v4.feature | 30 +- tests/integration/v4/forms/forms.v4.feature | 271 ++++++++++++++++++ v4/Http/Controllers/SurveyController.php | 150 +++++++--- v4/Models/Attribute.php | 11 + v4/Models/Stage.php | 10 +- v4/Models/Survey.php | 59 ++++ v4/Models/Translation.php | 38 +++ v4/Providers/MorphServiceProvider.php | 18 ++ 10 files changed, 549 insertions(+), 58 deletions(-) create mode 100644 migrations/20200506131856_add_entity_translations.php create mode 100644 tests/integration/v4/forms/forms.v4.feature create mode 100644 v4/Models/Translation.php create mode 100644 v4/Providers/MorphServiceProvider.php diff --git a/bootstrap/lumen.php b/bootstrap/lumen.php index 06f38f4332..c3b1262399 100644 --- a/bootstrap/lumen.php +++ b/bootstrap/lumen.php @@ -94,6 +94,7 @@ $app->register(Ushahidi\App\Providers\PassportServiceProvider::class); $app->register(Barryvdh\Cors\ServiceProvider::class); $app->register(Sentry\SentryLaravel\SentryLumenServiceProvider::class); +$app->register(v4\Providers\MorphServiceProvider::class); /* |-------------------------------------------------------------------------- diff --git a/migrations/20200506131856_add_entity_translations.php b/migrations/20200506131856_add_entity_translations.php new file mode 100644 index 0000000000..44b8aa2e89 --- /dev/null +++ b/migrations/20200506131856_add_entity_translations.php @@ -0,0 +1,19 @@ +table('translations') + ->addColumn('translatable_type', 'string', ['null' => false]) //form, attribute,stage,category + ->addColumn('translatable_id', 'integer') + ->addColumn('translated_key', 'string', ['null' => false]) //name, title, keys + ->addColumn('translation', 'string', ['null' => false]) //name, title, keys + ->addColumn('language', 'string', ['null' => false]) //name, title, keys + ->addTimestamps() + ->create(); + } +} diff --git a/tests/integration/v4/acl.v4.feature b/tests/integration/v4/acl.v4.feature index 4214173466..38a50e0a52 100644 --- a/tests/integration/v4/acl.v4.feature +++ b/tests/integration/v4/acl.v4.feature @@ -35,19 +35,19 @@ Feature: V4 API Access Control Layer And that the api_url is "api/v4" When I request "/surveys/8" Then the response is JSON - And the response has a "survey.stages" property - And the "survey.stages" property count is "4" - And the "survey.stages.0.attributes" property count is "2" - And the "survey.stages.2.attributes" property count is "0" + And the response has a "result.stages" property + And the "result.stages" property count is "4" + And the "result.stages.0.attributes" property count is "2" + And the "result.stages.2.attributes" property count is "0" Scenario: Listing All Stages for a form with hidden stages as a normal user Given that I want to find a "Survey" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" When I request "/surveys/8" Then the response is JSON - And the response has a "survey.stages" property - And the "survey.stages" property count is "1" - And the "survey.stages.0.attributes" property count is "0" + And the response has a "result.stages" property + And the "result.stages" property count is "1" + And the "result.stages.0.attributes" property count is "0" @rolesEnabled Scenario: User with Manage Settings permission can create a hydrated form Given that I want to make a new "Survey" @@ -101,14 +101,14 @@ Feature: V4 API Access Control Layer """ When I request "/surveys" Then the response is JSON - And the response has a "survey" property - And the response has a "survey.id" property - And the type of the "survey.id" property is "numeric" - And the response has a "survey.stages" property - And the type of the "survey.stages" property is "array" - And the response has a "survey.stages.0.attributes" property - And the "survey.stages.0.attributes" property count is "2" - And the "survey.name" property equals "new" + And the response has a "result" property + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the response has a "result.stages" property + And the type of the "result.stages" property is "array" + And the response has a "result.stages.0.attributes" property + And the "result.stages.0.attributes" property count is "2" + And the "result.name" property equals "new" Then the guzzle status code should be 200 @rolesEnabled Scenario: Basic user CANNOT create a hydrated form diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature new file mode 100644 index 0000000000..feca00fd89 --- /dev/null +++ b/tests/integration/v4/forms/forms.v4.feature @@ -0,0 +1,271 @@ +@oauth2Skip +Feature: Testing the Surveys API + + Scenario: Creating a new Survey + Given that I want to make a new "Survey" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "name":"Test Survey", + "type":"report", + "description":"This is a test form from BDD testing", + "disabled":false + } + """ + When I request "/surveys" + Then the response is JSON + And the response has a "result" property + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.disabled" property is false + And the "result.require_approval" property is true + And the "result.require_approval" property is true + And the response has a "result.everyone_can_create" property + And the "result.everyone_can_create" property is true + And the response has a "result.can_create" property + And the "result.can_create" property is empty + Then the guzzle status code should be 200 +# +# Scenario: Updating a Survey +# Given that I want to update a "Survey" +# And that the api_url is "api/v4" +# And that the request "data" is: +# """ +# { +# "name":"Updated Test Survey", +# "type":"report", +# "description":"This is a test form updated by BDD testing", +# "disabled":true, +# "require_approval":false, +# "everyone_can_create":false, +# "tags": [1,2,3,"junk"] +# } +# """ +# And that its "id" is "1" +# When I request "/surveys" +# Then the response is JSON +# And the response has a "result.id" property +# And the type of the "result.id" property is "numeric" +# And the "result.id" property equals "1" +# And the response has a "result.name" property +# And the "result.name" property equals "Updated Test Survey" +# And the "result.disabled" property is true +# And the "result.require_approval" property is false +# And the "result.everyone_can_create" property is false +# Then the guzzle status code should be 200 +# +# Scenario: Updating a Survey to clear name should fail +# Given that I want to update a "Survey" +# And that the api_url is "api/v4" +# And that the request "data" is: +# """ +# { +# "name":"", +# "type":"report", +# "description":"This is a test form updated by BDD testing", +# "disabled":true, +# "require_approval":false, +# "everyone_can_create":false +# } +# """ +# And that its "id" is "1" +# When I request "/surveys" +# Then the response is JSON +# Then the guzzle status code should be 422 +# +# Scenario: Update a non-existent Survey +# Given that I want to update a "Survey" +# And that the api_url is "api/v4" +# And that the request "data" is: +# """ +# { +# "name":"Updated Test Survey", +# "type":"report", +# "description":"This is a test form updated by BDD testing", +# "disabled":false +# } +# """ +# And that its "id" is "40" +# When I request "/surveys" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 404 +# + Scenario: Listing All Surveys + Given that I want to get all "Surveys" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the "results" property count is "9" + Then the guzzle status code should be 200 + + Scenario: Finding a Survey + Given that I want to find a "Survey" + And that its "id" is "1" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + Then the guzzle status code should be 200 + + Scenario: Finding a non-existent Survey + Given that I want to find a "Survey" + And that the api_url is "api/v4" + And that its "id" is "35" + When I request "/surveys" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 404 +## +## Scenario: POST method disabled for Survey Roles +## Given that I want to make a new "SurveyRole" +## And that the api_url is "api/v4" +## And that the request "data" is: +## """ +## { +## "roles": [1] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "errors" property +## Then the guzzle status code should be 405 +## +## Scenario: DELETE method disabled for Survey Roles +## Given that I want to delete a "SurveyRole" +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "errors" property +## Then the guzzle status code should be 405 +## +## Scenario: Add 1 role to Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [1] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "count" property +## And the "count" property equals "1" +## Then the guzzle status code should be 200 +## +## Scenario: Add 2 roles to Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [1,2] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "count" property +## And the "count" property equals "2" +## Then the guzzle status code should be 200 +## +## Scenario: Finding a Survey after roles have been set. +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [1,2] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## Given that I want to find a "Survey" +## And that its "id" is "1" +## When I request "/surveys" +## Then the response is JSON +## And the response has a "id" property +## And the type of the "id" property is "numeric" +## And the response has a "can_create" property +## And the response has a "can_create.0" property +## And the "can_create.0" property equals "user" +## And the response has a "can_create.1" property +## And the "can_create.1" property equals "admin" +## Then the guzzle status code should be 200 +## +## Scenario: Remove roles from Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "count" property +## And the "count" property equals "0" +## Then the guzzle status code should be 200 +## +## Scenario: Finding all Survey Roles +## Given that I want to find a "SurveyRole" +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "count" property +## And the type of the "count" property is "2" +## Then the guzzle status code should be 200 +## +## Scenario: Fail to add 1 invalid Role to Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [120] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "errors" property +## Then the guzzle status code should be 422 +## +## Scenario: Fail to add roles with 1 invalid Role id to Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [1,2,120] +## } +## """ +## When I request "/surveys/1/roles" +## Then the response is JSON +## And the response has a "errors" property +## Then the guzzle status code should be 422 +## +## Scenario: Add roles to non-existent Survey +## Given that I want to update a "SurveyRole" +## And that the request "data" is: +## """ +## { +## "roles": [1] +## } +## """ +## When I request "/surveys/26/roles" +## Then the response is JSON +## And the response has a "errors" property +## Then the guzzle status code should be 404 +# +# Scenario: Delete a Survey +# Given that I want to delete a "Survey" +# And that the api_url is "api/v4" +# And that its "id" is "1" +# When I request "/surveys" +# Then the response is JSON +# And the response has a "id" property +# Then the guzzle status code should be 200 +# +# Scenario: Fail to delete a non existent Survey +# Given that I want to delete a "Survey" +# And that the api_url is "api/v4" +# And that its "id" is "35" +# When I request "/surveys" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 404 diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 28c514a6ba..e46e32a632 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -13,35 +13,8 @@ class SurveyController extends V4Controller { - /** - * Display the specified resource. - * - * @param int $id - * @return \Illuminate\Http\JsonResponse - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function show(int $id) - { - $survey = Survey::find($id); - $this->authorize('show', $survey); - return response()->json(['survey' => $survey]); - } - - /** - * Display the specified resource. - * - * @return \Illuminate\Http\JsonResponse - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index() - { - $this->authorize('index', Survey::class); - return response()->json(['results' => Survey::all()]); - } - - public function store(Request $request) { - $this->authorize('store', Survey::class); - $validator = $this->getValidationFactory()->make($request->input(), [ + protected static function getRules() { + return [ 'name' => [ 'required', 'min:2', @@ -156,28 +129,121 @@ public function store(Request $request) { // Before merge, validate with Angela if we // should be removing that arbitrary limit since it's pretty rare // for it to be needed - ]); + ]; + } + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function show(int $id) + { + $survey = Survey::with('translations')->find($id); + $not_found = !$survey; + if ($not_found) { + $survey = new Survey(); + } + // we try to authorize even if we don't find a survey + // this allows us to return a 404 to users who would + // be allowed to read surveys and a 403 to those who wouldn't + // obfuscating the existence of particular unauthorized surveys + // or non-existent ones to users without any permissions to see them + $this->authorize('show', $survey); + if ($not_found) { + abort(404); + } + return response()->json(['result' => $survey]); + } + + /** + * Display the specified resource. + * @TODO add translation keys to each object =) + * @TODO add enabled_languages (the ones that we have translations for) + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index() + { + $this->authorize('index', Survey::class); + return response()->json(['results' => Survey::all()]); + } + + /** + * Display the specified resource. + * @TODO add translation keys to each object =) + * @TODO add enabled_languages (the ones that we have translations for) + * @TODO transactions =) + * @param Request $request + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function store(Request $request) { + $this->authorize('store', Survey::class); + $this->getValidationFactory()->make($request->input(), self::getRules()); + $survey = Survey::create( + array_merge( + $request->input(),[ 'updated' => time(), 'created' => time()] + ) + ); + if ($request->input('stages')) { + foreach ($request->input('stages') as $stage) { + $stage_model = $survey->stages()->create( + array_merge( + $stage, [ 'updated' => time(), 'created' => time()] + ) + ); + foreach ($stage['attributes'] as $attribute) { + $uuid = Uuid::uuid4(); + $attribute['key'] = $uuid->toString(); + $stage_model->attributes()->create( + array_merge( + $attribute, [ 'updated' => time(), 'created' => time()] + ) + ); + } + } + } + return response()->json(['result' => $survey->load('stages')]); + } + + /** + * Display the specified resource. + * @TODO add translation keys to each object =) + * @TODO add enabled_languages (the ones that we have translations for) + * @TODO transactions =) + * @param Request $request + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function update(int $id, Request $request) { + $survey = Survey::find($id); + $this->authorize('update', $survey); + $this->getValidationFactory()->make($request->input(), self::getRules()); $survey = Survey::create( array_merge( $request->input(),[ 'updated' => time(), 'created' => time()] ) ); - foreach ($request->input('stages') as $stage) { - $stage_model = $survey->stages()->create( - array_merge( - $stage, [ 'updated' => time(), 'created' => time()] - ) - ); - foreach ($stage['attributes'] as $attribute) { - $uuid = Uuid::uuid4(); - $attribute['key'] = $uuid->toString(); - $stage_model->attributes()->create( + if ($request->input('stages')) { + foreach ($request->input('stages') as $stage) { + $stage_model = $survey->stages()->create( array_merge( - $attribute, [ 'updated' => time(), 'created' => time()] + $stage, [ 'updated' => time(), 'created' => time()] ) ); + foreach ($stage['attributes'] as $attribute) { + $uuid = Uuid::uuid4(); + $attribute['key'] = $uuid->toString(); + $stage_model->attributes()->create( + array_merge( + $attribute, [ 'updated' => time(), 'created' => time()] + ) + ); + } } } - return response()->json(['survey' => $survey->load('stages')]); + return response()->json(['result' => $survey->load('stages')]); } } diff --git a/v4/Models/Attribute.php b/v4/Models/Attribute.php index d5b396136d..b805e86737 100644 --- a/v4/Models/Attribute.php +++ b/v4/Models/Attribute.php @@ -35,12 +35,23 @@ class Attribute extends Model 'response_private', 'form_stage_id' ]; + protected $with = ['translations']; + protected $casts = [ 'config' => 'json', 'options' => 'json', ]; + public function stage () { return $this->belongsTo('v4\Models\Stage', 'form_stage_id'); } + + /** + * Get the attribute's translation. + */ + public function translations() + { + return $this->morphMany('v4\Models\Translation', 'translatable'); + } } diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index f8e3306bc0..baa5aa29f8 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -15,7 +15,7 @@ class Stage extends Model * @var array */ protected $dates = ['created', 'updated']; - protected $with = ['attributes']; + protected $with = ['attributes', 'translations']; /** * The attributes that are mass assignable. * @@ -42,4 +42,12 @@ public function survey() { return $this->belongsTo('v4\Models\Survey', 'form_id'); } + /** + * Get the stage's translation. + */ + public function translations() + { + return $this->morphMany('v4\Models\Translation', 'translatable'); + } + } diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index b377827dda..9e93314812 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -3,6 +3,7 @@ namespace v4\Models; use Illuminate\Database\Eloquent\Model; +use Ushahidi\App\Repository\FormRepository; use Ushahidi\Core\Entity\Permission; use Ushahidi\Core\Tool\Permissions\InteractsWithFormPermissions; @@ -46,6 +47,54 @@ class Survey extends Model 'targeted_survey' ]; + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = ['can_create']; + /** + * The model's default values for attributes. + * + * @var array + */ + protected $attributes = [ + 'type' => 'report', + 'require_approval' => true, + 'everyone_can_create' => true, + 'hide_author' => false, + 'hide_time' => false, + 'hide_location' => false, + 'targeted_survey' => false + ]; + + /** + * This is what makes can_create possible + * @return mixed + */ + public function getCanCreateAttribute() { + $can_create = $this->getCanCreateRoles($this->id); + return $can_create['roles']; + } +// +// /** +// * This is what makes can_create possible +// * @return mixed +// */ +// public function getTranslationsAttribute() { +// return $this->translations; +// } + + private function getCanCreateRoles($form_id) { + /** + * @NOTE: to lower changes of a regression I'm using some helpers from + * repositories and traits we already have + * @NOTE: during origami and later stages of sunny buffers, we will fold this + * all together in more performant and friendly ways + */ + $form_repo = service('repository.form'); + return $form_repo->getRolesThatCanCreatePosts($form_id); + } /** * We check for relationship permissions here, to avoid hydrating anything that should not be hydrated. * @return \Illuminate\Database\Eloquent\Relations\HasMany @@ -65,4 +114,14 @@ public function stages() ->where('form_stages.task_is_internal_only', '=', '0'); } + + + + /** + * Get the survey's translation. + */ + public function translations() + { + return $this->morphMany('v4\Models\Translation', 'translatable'); + } } diff --git a/v4/Models/Translation.php b/v4/Models/Translation.php new file mode 100644 index 0000000000..02f2e1aea6 --- /dev/null +++ b/v4/Models/Translation.php @@ -0,0 +1,38 @@ +morphTo(); + } +} diff --git a/v4/Providers/MorphServiceProvider.php b/v4/Providers/MorphServiceProvider.php new file mode 100644 index 0000000000..46107ac42f --- /dev/null +++ b/v4/Providers/MorphServiceProvider.php @@ -0,0 +1,18 @@ + 'v4\Models\Survey', + 'stage' => 'v4\Models\Stage', + 'attribute' => 'v4\Models\Attribute', + ]); + } +} From 62a1ca13c13b4f3262da1885148e4506216ad37c Mon Sep 17 00:00:00 2001 From: rowasc Date: Wed, 6 May 2020 22:59:30 +0000 Subject: [PATCH 11/39] Delete placeholder migration --- ...ll_export_batch_filename copy.php.ignoreme | 134 ------------------ 1 file changed, 134 deletions(-) delete mode 100644 migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme diff --git a/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme b/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme deleted file mode 100644 index 497901595a..0000000000 --- a/migrations/20201206211859_allow_null_export_batch_filename copy.php.ignoreme +++ /dev/null @@ -1,134 +0,0 @@ -table('translations') - ->addColumn('id') - ->addColumn('entity_type') //form, attribute,stage,category - ->addColumn('entity_id') - ->addColumn('translated_key')// ??? - ->addColumn('translation') - ->addColumn('language') - ->addTimestamps() - ->create(); - - // saving a form translation - - $table->insert([ - 'entity_type' => 'form', - 'entity_id' => 1, - 'translated_key' => 'name', - 'translation' => 'Nombre de la encuesta', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form', - 'entity_id' => 1, - 'translated_key' => 'description', - 'translation' => 'Descripcion de la encuesta', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => '1',//form_attribute_id for title - 'translated_key' => 'label', - 'translation' => 'El field title of the form translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 2,//form_attribute_id for description - 'translated_key' => 'label',//form_attributes.label - 'translation' => 'El field description translated', - 'language' => 'es-ES' - ]); - - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 2,//form_attribute_id for description - 'translated_key' => 'description',//form_attributes.description - 'translation' => 'The description for the description field :D', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 1,//form_attribute_id - 'translated_key' => 'label',//select - 'translation' => 'El field select translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 1,//form_attribute_id - 'translated_key' => 'description',//select - 'translation' => 'El field select translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 1,//form_attribute_id - 'translated_key' => 'default',//select - 'translation' => 'El field select translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_attributes', - 'entity_id' => 1,//form_attribute_id - 'translated_key' => 'options',//select - 'translation' => '["El field select translated", "Option 2"]', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_stages', - 'entity_id' => 4,//form_stage_id - 'translated_key' => 'name',//task name - 'translation' => 'El name del task translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'form_stages', - 'entity_id' => 4,//form_stage_id - 'translated_key' => 'description',//task name - 'translation' => 'El description del task translated', - 'language' => 'es-ES' - ]); - - - $table->insert([ - 'entity_type' => 'tags', - 'entity_id' => 46,//form_stage_id - 'translated_key' => 'name',//task name - 'translation' => 'El name del tag translated', - 'language' => 'es-ES' - ]); - - $table->insert([ - 'entity_type' => 'tags', - 'entity_id' => 46,//form_stage_id - 'translated_key' => 'description',//task name - 'translation' => 'El description del tag translated', - 'language' => 'es-ES' - ]); - - - } - - public function down() - { - // No op. Don't reverse this or it causes bugs - } -} From a7a2ba1ea721e8891ecaec0fdaceef56075d39f9 Mon Sep 17 00:00:00 2001 From: rowasc Date: Wed, 6 May 2020 23:01:23 +0000 Subject: [PATCH 12/39] Delete placeholder migration comments --- tests/datasets/ushahidi/# Entity translations | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 tests/datasets/ushahidi/# Entity translations diff --git a/tests/datasets/ushahidi/# Entity translations b/tests/datasets/ushahidi/# Entity translations deleted file mode 100644 index 3dd4c567a9..0000000000 --- a/tests/datasets/ushahidi/# Entity translations +++ /dev/null @@ -1,114 +0,0 @@ -# Entity translations - - -### Site wide configuration for available languages - -Add the enabled_languages key and object - -PUT http://192.168.33.110/api/v3/config/site -``` -{ - "id": "site", - "url": "http://192.168.33.110/api/v3/config/site", - "enabled_languages": { - "default": "en-US", - "available": ["es-ES", "pt-BR"] - }, - "name": "Deployer", - "description": "Hello!", - "email": "", - "timezone": "UTC", - "language": "en-US", - "date_format": "n/j/Y", - "client_url": false, - "first_login": true, - "tier": "free", - "private": false, - "allowed_privileges": [ - "read", - "search" - ] -} -``` - -## Sending a new survey with language -Use the default /forms endpoint, add support for enabled_languages. -The enabled_languages key will be required, with "default" being "en-EN" and "available" being an empty array on creation. -We will run a migration to add this field and value to all current surveys. -When we release, we will let users know of this feature through intercom so that they can go and ajust the default language manually. - -POST http://192.168.33.110/api/v3/forms - -``` -{ - "enabled_languages": { - "default": "en-EN", - "available" : [] - }, - "color": null, - "require_approval": true, - "everyone_can_create": false, - "tasks": [ - { - "label": "Post", - "priority": 0, - "required": false, - "type": "post", - "show_when_published": true, - "task_is_internal_only": false, - "attributes": [ - { - "cardinality": 0, - "input": "text", - "label": "Title", - "priority": 1, - "required": true, - "type": "title", - "options": [], - "config": {}, - "form_stage_id": "interim_id_0" - }, - { - "cardinality": 0, - "input": "text", - "label": "Description", - "priority": 2, - "required": true, - "type": "description", - "options": [], - "config": {}, - "form_stage_id": "interim_id_1" - } - ], - "is_public": true, - "id": "interim_id_2" - } - ], - "name": "Hello world", - "description": "A survey that I plan on translating" -} -``` -PUT http://192.168.33.110/api/v3/forms/{id}/translate - -Receives the translated attribute fields, stages, form fields (desc/title) and the language it's being translated to. - -{ - "form_fields": { - "name": "Hola mundo", - "description": "Una encuesta que planeo a traducir." - }, - "stages": [ - { - "id": 1, - "attributes": { - "{key}": { - "label": "Una etiqueta", //name - "instructions": "Instrucciones para el campo" //description - } - }, - "description": "La descripcion del stage", - "label": "El nombre del stage" - } - ] - "language": "es-ES" -} From 1bf1d43f7626b4a34941e8a53e10c76a27bc631a Mon Sep 17 00:00:00 2001 From: Romina Date: Thu, 7 May 2020 10:50:07 -0300 Subject: [PATCH 13/39] Add languages endpoint --- tests/integration/v4/translations.v4.feature | 96 ++++++++++++++++++++ v4/Http/Controllers/LanguagesController.php | 19 ++++ v4/routes/web.php | 7 ++ 3 files changed, 122 insertions(+) create mode 100644 tests/integration/v4/translations.v4.feature create mode 100644 v4/Http/Controllers/LanguagesController.php diff --git a/tests/integration/v4/translations.v4.feature b/tests/integration/v4/translations.v4.feature new file mode 100644 index 0000000000..2af79ca877 --- /dev/null +++ b/tests/integration/v4/translations.v4.feature @@ -0,0 +1,96 @@ +@acl + @rolesEnabled + Scenario: User with Manage Settings permission can create a hydrated form + Given that I want to make a new "Survey" + And that the oauth token is "testmanager" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "enabled_languages": { + "default": "en-EN", + "available": ["es"] + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + 'translations': { + "es": + { + "name": "nombre", + } + ] + }, + // TODO group translations by language + "stages": [ + { + "translations": { + "es": { + "label": "Reporte", + "description": "Una descripcion" + } + }, + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "attributes": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {}, + "translations": { + "es": { + "label": "Titulo", + "instructions": "Instrucciones", + "default": "Un valor por defecto", + "options": ["Una opcion", "otra opcion"] + } + }, + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {}, + "translations": { + "es": { + "label": "Descripcion", + "instructions": "Instrucciones de la desc", + "default": "Un valor por defecto para desc", + "options": ["Una opcion", "otra opcion desc"] + } + }, + } + ], + "is_public": true + } + ], + "name": "new" + } + """ + When I request "/surveys" + Then the response is JSON + And the response has a "result" property + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the response has a "result.stages" property + And the type of the "result.stages" property is "array" + And the response has a "result.stages.0.attributes" property + And the "result.stages.0.attributes" property count is "2" + And the "result.translations.es.0.name" property equals "label" + And the "result.stages.0.translations.es.label" property equals "Reporte" + And the "result.name" property equals "new" + Then the guzzle status code should be 200 diff --git a/v4/Http/Controllers/LanguagesController.php b/v4/Http/Controllers/LanguagesController.php new file mode 100644 index 0000000000..f4510ca5d1 --- /dev/null +++ b/v4/Http/Controllers/LanguagesController.php @@ -0,0 +1,19 @@ + "ach", "name" => "Acoli"], ["code" => "ady", "name" => "Adyghe"], ["code" => "af", "name" => "Afrikaans"], ["code" => "af-ZA", "name" => "Afrikaans (South Africa)"], ["code" => "ak", "name" => "Akan"], ["code" => "sq", "name" => "Albanian"], ["code" => "sq-AL", "name" => "Albanian (Albania)"], ["code" => "aln", "name" => "Albanian Gheg"], ["code" => "am", "name" => "Amharic"], ["code" => "am-ET", "name" => "Amharic (Ethiopia)"], ["code" => "ar", "name" => "Arabic"], ["code" => "ar-EG", "name" => "Arabic (Egypt)"], ["code" => "ar-SA", "name" => "Arabic (Saudi Arabia)"], ["code" => "ar-SD", "name" => "Arabic (Sudan)"], ["code" => "ar-SY", "name" => "Arabic (Syria)"], ["code" => "ar-AA", "name" => "Arabic (Unitag)"], ["code" => "an", "name" => "Aragonese"], ["code" => "hy", "name" => "Armenian"], ["code" => "hy-AM", "name" => "Armenian (Armenia)"], ["code" => "as", "name" => "Assamese"], ["code" => "as-IN", "name" => "Assamese (India)"], ["code" => "ast", "name" => "Asturian"], ["code" => "ast-ES", "name" => "Asturian (Spain)"], ["code" => "az", "name" => "Azerbaijani"], ["code" => "az@Arab", "name" => "Azerbaijani (Arabic)"], ["code" => "az-AZ", "name" => "Azerbaijani (Azerbaijan)"], ["code" => "az-IR", "name" => "Azerbaijani (Iran)"], ["code" => "az@latin", "name" => "Azerbaijani (Latin)"], ["code" => "bal", "name" => "Balochi"], ["code" => "ba", "name" => "Bashkir"], ["code" => "eu", "name" => "Basque"], ["code" => "eu-ES", "name" => "Basque (Spain)"], ["code" => "bar", "name" => "Bavarian"], ["code" => "be", "name" => "Belarusian"], ["code" => "be-BY", "name" => "Belarusian (Belarus)"], ["code" => "be@tarask", "name" => "Belarusian (Tarask)"], ["code" => "bn", "name" => "Bengali"], ["code" => "bn-BD", "name" => "Bengali (Bangladesh)"], ["code" => "bn-IN", "name" => "Bengali (India)"], ["code" => "brx", "name" => "Bodo"], ["code" => "bs", "name" => "Bosnian"], ["code" => "bs-BA", "name" => "Bosnian (Bosnia and Herzegovina)"], ["code" => "br", "name" => "Breton"], ["code" => "bg", "name" => "Bulgarian"], ["code" => "bg-BG", "name" => "Bulgarian (Bulgaria)"], ["code" => "my", "name" => "Burmese"], ["code" => "my-MM", "name" => "Burmese (Myanmar)"], ["code" => "ca", "name" => "Catalan"], ["code" => "ca-ES", "name" => "Catalan (Spain)"], ["code" => "ca@valencia", "name" => "Catalan (Valencian)"], ["code" => "ceb", "name" => "Cebuano"], ["code" => "tzm", "name" => "Central Atlas Tamazight"], ["code" => "hne", "name" => "Chhattisgarhi"], ["code" => "cgg", "name" => "Chiga"], ["code" => "zh", "name" => "Chinese"], ["code" => "zh-CN", "name" => "Chinese (China)"], ["code" => "zh-CN.GB2312", "name" => "Chinese (China) (GB2312)"], ["code" => "gan", "name" => "Chinese (Gan)"], ["code" => "hak", "name" => "Chinese (Hakka)"], ["code" => "zh-HK", "name" => "Chinese (Hong Kong)"], ["code" => "czh", "name" => "Chinese (Huizhou)"], ["code" => "cjy", "name" => "Chinese (Jinyu)"], ["code" => "lzh", "name" => "Chinese (Literary)"], ["code" => "cmn", "name" => "Chinese (Mandarin)"], ["code" => "mnp", "name" => "Chinese (Min Bei)"], ["code" => "cdo", "name" => "Chinese (Min Dong)"], ["code" => "nan", "name" => "Chinese (Min Nan)"], ["code" => "czo", "name" => "Chinese (Min Zhong)"], ["code" => "cpx", "name" => "Chinese (Pu-Xian)"], ["code" => "zh-Hans", "name" => "Chinese Simplified"], ["code" => "zh-TW", "name" => "Chinese (Taiwan)"], ["code" => "zh-TW.Big5", "name" => "Chinese (Taiwan) (Big5) "], ["code" => "zh-Hant", "name" => "Chinese Traditional"], ["code" => "wuu", "name" => "Chinese (Wu)"], ["code" => "hsn", "name" => "Chinese (Xiang)"], ["code" => "yue", "name" => "Chinese (Yue)"], ["code" => "cv", "name" => "Chuvash"], ["code" => "ksh", "name" => "Colognian"], ["code" => "kw", "name" => "Cornish"], ["code" => "co", "name" => "Corsican"], ["code" => "crh", "name" => "Crimean Turkish"], ["code" => "hr", "name" => "Croatian"], ["code" => "hr-HR", "name" => "Croatian (Croatia)"], ["code" => "cs", "name" => "Czech"], ["code" => "cs-CZ", "name" => "Czech (Czech Republic)"], ["code" => "da", "name" => "Danish"], ["code" => "da-DK", "name" => "Danish (Denmark)"], ["code" => "dv", "name" => "Divehi"], ["code" => "doi", "name" => "Dogri"], ["code" => "nl", "name" => "Dutch"], ["code" => "nl-BE", "name" => "Dutch (Belgium)"], ["code" => "nl-NL", "name" => "Dutch (Netherlands)"], ["code" => "dz", "name" => "Dzongkha"], ["code" => "dz-BT", "name" => "Dzongkha (Bhutan)"], ["code" => "en", "name" => "English"], ["code" => "en-AU", "name" => "English (Australia)"], ["code" => "en-AT", "name" => "English (Austria)"], ["code" => "en-BD", "name" => "English (Bangladesh)"], ["code" => "en-BE", "name" => "English (Belgium)"], ["code" => "en-CA", "name" => "English (Canada)"], ["code" => "en-CL", "name" => "English (Chile)"], ["code" => "en-CZ", "name" => "English (Czech Republic)"], ["code" => "en-ee", "name" => "English (Estonia)"], ["code" => "en-FI", "name" => "English (Finland)"], ["code" => "en-DE", "name" => "English (Germany)"], ["code" => "en-GH", "name" => "English (Ghana)"], ["code" => "en-HK", "name" => "English (Hong Kong)"], ["code" => "en-HU", "name" => "English (Hungary)"], ["code" => "en-IN", "name" => "English (India)"], ["code" => "en-IE", "name" => "English (Ireland)"], ["code" => "en-lv", "name" => "English (Latvia)"], ["code" => "en-lt", "name" => "English (Lithuania)"], ["code" => "en-NL", "name" => "English (Netherlands)"], ["code" => "en-NZ", "name" => "English (New Zealand)"], ["code" => "en-NG", "name" => "English (Nigeria)"], ["code" => "en-PK", "name" => "English (Pakistan)"], ["code" => "en-PL", "name" => "English (Poland)"], ["code" => "en-RO", "name" => "English (Romania)"], ["code" => "en-SK", "name" => "English (Slovakia)"], ["code" => "en-ZA", "name" => "English (South Africa)"], ["code" => "en-LK", "name" => "English (Sri Lanka)"], ["code" => "en-SE", "name" => "English (Sweden)"], ["code" => "en-CH", "name" => "English (Switzerland)"], ["code" => "en-GB", "name" => "English (United Kingdom)"], ["code" => "en-US", "name" => "English (United States)"], ["code" => "en-EN", "name" => "English"], ["code" => "myv", "name" => "Erzya"], ["code" => "eo", "name" => "Esperanto"], ["code" => "et", "name" => "Estonian"], ["code" => "et-EE", "name" => "Estonian (Estonia)"], ["code" => "fo", "name" => "Faroese"], ["code" => "fo-FO", "name" => "Faroese (Faroe Islands)"], ["code" => "fil", "name" => "Filipino"], ["code" => "fi", "name" => "Finnish"], ["code" => "fi-FI", "name" => "Finnish (Finland)"], ["code" => "frp", "name" => "Franco-Provençal (Arpitan)"], ["code" => "fr", "name" => "French"], ["code" => "fr-BE", "name" => "French (Belgium)"], ["code" => "fr-CA", "name" => "French (Canada)"], ["code" => "fr-FR", "name" => "French (France)"], ["code" => "fr-CH", "name" => "French (Switzerland)"], ["code" => "fur", "name" => "Friulian"], ["code" => "ff", "name" => "Fulah"], ["code" => "ff-SN", "name" => "Fulah (Senegal)"], ["code" => "gd", "name" => "Gaelic, Scottish"], ["code" => "gl", "name" => "Galician"], ["code" => "gl-ES", "name" => "Galician (Spain)"], ["code" => "lg", "name" => "Ganda"], ["code" => "ka", "name" => "Georgian"], ["code" => "ka-GE", "name" => "Georgian (Georgia)"], ["code" => "de", "name" => "German"], ["code" => "de-AT", "name" => "German (Austria)"], ["code" => "de-DE", "name" => "German (Germany)"], ["code" => "de-CH", "name" => "German (Switzerland)"], ["code" => "el", "name" => "Greek"], ["code" => "el-GR", "name" => "Greek (Greece)"], ["code" => "kl", "name" => "Greenlandic"], ["code" => "gu", "name" => "Gujarati"], ["code" => "gu-IN", "name" => "Gujarati (India)"], ["code" => "gun", "name" => "Gun"], ["code" => "ht", "name" => "Haitian (Haitian Creole)"], ["code" => "ht-HT", "name" => "Haitian (Haitian Creole) (Haiti)"], ["code" => "ha", "name" => "Hausa"], ["code" => "haw", "name" => "Hawaiian"], ["code" => "he", "name" => "Hebrew"], ["code" => "he-IL", "name" => "Hebrew (Israel)"], ["code" => "hi", "name" => "Hindi"], ["code" => "hi-IN", "name" => "Hindi (India)"], ["code" => "hu", "name" => "Hungarian"], ["code" => "hu-HU", "name" => "Hungarian (Hungary)"], ["code" => "is", "name" => "Icelandic"], ["code" => "is-IS", "name" => "Icelandic (Iceland)"], ["code" => "io", "name" => "Ido"], ["code" => "ig", "name" => "Igbo"], ["code" => "ilo", "name" => "Iloko"], ["code" => "id", "name" => "Indonesian"], ["code" => "id-ID", "name" => "Indonesian (Indonesia)"], ["code" => "ia", "name" => "Interlingua"], ["code" => "iu", "name" => "Inuktitut"], ["code" => "ga", "name" => "Irish"], ["code" => "ga-IE", "name" => "Irish (Ireland)"], ["code" => "it", "name" => "Italian"], ["code" => "it-IT", "name" => "Italian (Italy)"], ["code" => "it-CH", "name" => "Italian (Switzerland)"], ["code" => "ja", "name" => "Japanese"], ["code" => "ja-JP", "name" => "Japanese (Japan)"], ["code" => "jv", "name" => "Javanese"], ["code" => "kab", "name" => "Kabyle"], ["code" => "kn", "name" => "Kannada"], ["code" => "kn-IN", "name" => "Kannada (India)"], ["code" => "pam", "name" => "Kapampangan"], ["code" => "ks", "name" => "Kashmiri"], ["code" => "ks-IN", "name" => "Kashmiri (India)"], ["code" => "csb", "name" => "Kashubian"], ["code" => "kk", "name" => "Kazakh"], ["code" => "kk@Arab", "name" => "Kazakh (Arabic)"], ["code" => "kk@Cyrl", "name" => "Kazakh (Cyrillic)"], ["code" => "kk-KZ", "name" => "Kazakh (Kazakhstan)"], ["code" => "kk@latin", "name" => "Kazakh (Latin)"], ["code" => "km", "name" => "Khmer"], ["code" => "km-KH", "name" => "Khmer (Cambodia)"], ["code" => "rw", "name" => "Kinyarwanda"], ["code" => "ky", "name" => "Kirgyz"], ["code" => "tlh", "name" => "Klingon"], ["code" => "kok", "name" => "Konkani"], ["code" => "ko", "name" => "Korean"], ["code" => "ko-KR", "name" => "Korean (Korea)"], ["code" => "ku", "name" => "Kurdish"], ["code" => "ku-IQ", "name" => "Kurdish (Iraq)"], ["code" => "lad", "name" => "Ladino"], ["code" => "lo", "name" => "Lao"], ["code" => "lo-LA", "name" => "Lao (Laos)"], ["code" => "ltg", "name" => "Latgalian"], ["code" => "la", "name" => "Latin"], ["code" => "lv", "name" => "Latvian"], ["code" => "lv-LV", "name" => "Latvian (Latvia)"], ["code" => "lez", "name" => "Lezghian"], ["code" => "lij", "name" => "Ligurian"], ["code" => "li", "name" => "Limburgian"], ["code" => "ln", "name" => "Lingala"], ["code" => "lt", "name" => "Lithuanian"], ["code" => "lt-LT", "name" => "Lithuanian (Lithuania)"], ["code" => "jbo", "name" => "Lojban"], ["code" => "en@lolcat", "name" => "LOLCAT English"], ["code" => "lmo", "name" => "Lombard"], ["code" => "dsb", "name" => "Lower Sorbian"], ["code" => "nds", "name" => "Low German"], ["code" => "lb", "name" => "Luxembourgish"], ["code" => "mk", "name" => "Macedonian"], ["code" => "mk-MK", "name" => "Macedonian (Macedonia)"], ["code" => "mai", "name" => "Maithili"], ["code" => "mg", "name" => "Malagasy"], ["code" => "ms", "name" => "Malay"], ["code" => "ml", "name" => "Malayalam"], ["code" => "ml-IN", "name" => "Malayalam (India)"], ["code" => "ms-MY", "name" => "Malay (Malaysia)"], ["code" => "mt", "name" => "Maltese"], ["code" => "mt-MT", "name" => "Maltese (Malta)"], ["code" => "mni", "name" => "Manipuri"], ["code" => "mi", "name" => "Maori"], ["code" => "arn", "name" => "Mapudungun"], ["code" => "mr", "name" => "Marathi"], ["code" => "mr-IN", "name" => "Marathi (India)"], ["code" => "mh", "name" => "Marshallese"], ["code" => "mw1", "name" => "Mirandese"], ["code" => "mn", "name" => "Mongolian"], ["code" => "mn-MN", "name" => "Mongolian (Mongolia)"], ["code" => "nah", "name" => "Nahuatl"], ["code" => "nv", "name" => "Navajo"], ["code" => "nr", "name" => "Ndebele, South"], ["code" => "nap", "name" => "Neapolitan"], ["code" => "ne", "name" => "Nepali"], ["code" => "ne-NP", "name" => "Nepali (Nepal)"], ["code" => "nia", "name" => "Nias"], ["code" => "nqo", "name" => "N'ko"], ["code" => "se", "name" => "Northern Sami"], ["code" => "nso", "name" => "Northern Sotho"], ["code" => "no", "name" => "Norwegian"], ["code" => "nb", "name" => "Norwegian Bokmål"], ["code" => "nb-NO", "name" => "Norwegian Bokmål (Norway)"], ["code" => "no-NO", "name" => "Norwegian (Norway)"], ["code" => "nn", "name" => "Norwegian Nynorsk"], ["code" => "nn-NO", "name" => "Norwegian Nynorsk (Norway)"], ["code" => "ny", "name" => "Nyanja"], ["code" => "oc", "name" => "Occitan (post 1500)"], ["code" => "or", "name" => "Oriya"], ["code" => "or-IN", "name" => "Oriya (India)"], ["code" => "om", "name" => "Oromo"], ["code" => "os", "name" => "Ossetic"], ["code" => "pfl", "name" => "Palatinate German"], ["code" => "pa", "name" => "Panjabi (Punjabi)"], ["code" => "pa-IN", "name" => "Panjabi (Punjabi) (India)"], ["code" => "pap", "name" => "Papiamento"], ["code" => "fa", "name" => "Persian"], ["code" => "fa-AF", "name" => "Persian (Afghanistan)"], ["code" => "fa-IR", "name" => "Persian (Iran)"], ["code" => "pms", "name" => "Piemontese"], ["code" => "en@pirate", "name" => "Pirate English"], ["code" => "pl", "name" => "Polish"], ["code" => "pl-PL", "name" => "Polish (Poland)"], ["code" => "pt", "name" => "Portuguese"], ["code" => "pt-BR", "name" => "Portuguese (Brazil)"], ["code" => "pt-PT", "name" => "Portuguese (Portugal)"], ["code" => "ps", "name" => "Pushto"], ["code" => "ro", "name" => "Romanian"], ["code" => "ro-RO", "name" => "Romanian (Romania)"], ["code" => "rm", "name" => "Romansh"], ["code" => "ru", "name" => "Russian"], ["code" => "ru-ee", "name" => "Russian (Estonia)"], ["code" => "ru-lv", "name" => "Russian (Latvia)"], ["code" => "ru-lt", "name" => "Russian (Lithuania)"], ["code" => "ru@petr1708", "name" => "Russian Petrine orthography"], ["code" => "ru-RU", "name" => "Russian (Russia)"], ["code" => "sah", "name" => "Sakha (Yakut)"], ["code" => "sm", "name" => "Samoan"], ["code" => "sa", "name" => "Sanskrit"], ["code" => "sat", "name" => "Santali"], ["code" => "sc", "name" => "Sardinian"], ["code" => "sco", "name" => "Scots"], ["code" => "sr", "name" => "Serbian"], ["code" => "sr@Ijekavian", "name" => "Serbian (Ijekavian)"], ["code" => "sr@ijekavianlatin", "name" => "Serbian (Ijekavian Latin)"], ["code" => "sr@latin", "name" => "Serbian (Latin)"], ["code" => "sr-RS@latin", "name" => "Serbian (Latin) (Serbia)"], ["code" => "sr-RS", "name" => "Serbian (Serbia)"], ["code" => "sn", "name" => "Shona"], ["code" => "scn", "name" => "Sicilian"], ["code" => "szl", "name" => "Silesian"], ["code" => "sd", "name" => "Sindhi"], ["code" => "si", "name" => "Sinhala"], ["code" => "si-LK", "name" => "Sinhala (Sri Lanka)"], ["code" => "sk", "name" => "Slovak"], ["code" => "sk-SK", "name" => "Slovak (Slovakia)"], ["code" => "sl", "name" => "Slovenian"], ["code" => "sl-SI", "name" => "Slovenian (Slovenia)"], ["code" => "so", "name" => "Somali"], ["code" => "son", "name" => "Songhay"], ["code" => "st", "name" => "Sotho, Southern"], ["code" => "st-ZA", "name" => "Sotho, Southern (South Africa)"], ["code" => "sma", "name" => "Southern Sami"], ["code" => "es", "name" => "Spanish"], ["code" => "es-AR", "name" => "Spanish (Argentina)"], ["code" => "es-BO", "name" => "Spanish (Bolivia)"], ["code" => "es-CL", "name" => "Spanish (Chile)"], ["code" => "es-CO", "name" => "Spanish (Colombia)"], ["code" => "es-CR", "name" => "Spanish (Costa Rica)"], ["code" => "es-DO", "name" => "Spanish (Dominican Republic)"], ["code" => "es-EC", "name" => "Spanish (Ecuador)"], ["code" => "es-SV", "name" => "Spanish (El Salvador)"], ["code" => "es-GT", "name" => "Spanish (Guatemala)"], ["code" => "es-419", "name" => "Spanish (Latin America)"], ["code" => "es-MX", "name" => "Spanish (Mexico)"], ["code" => "es-NI", "name" => "Spanish (Nicaragua)"], ["code" => "es-PA", "name" => "Spanish (Panama)"], ["code" => "es-PY", "name" => "Spanish (Paraguay)"], ["code" => "es-PE", "name" => "Spanish (Peru)"], ["code" => "es-PR", "name" => "Spanish (Puerto Rico)"], ["code" => "es-ES", "name" => "Spanish (Spain)"], ["code" => "es-US", "name" => "Spanish (United States)"], ["code" => "es-UY", "name" => "Spanish (Uruguay)"], ["code" => "es-VE", "name" => "Spanish (Venezuela)"], ["code" => "su", "name" => "Sundanese"], ["code" => "sw", "name" => "Swahili"], ["code" => "sw-KE", "name" => "Swahili (Kenya)"], ["code" => "ss", "name" => "Swati"], ["code" => "sv", "name" => "Swedish"], ["code" => "sv-FI", "name" => "Swedish (Finland)"], ["code" => "sv-SE", "name" => "Swedish (Sweden)"], ["code" => "tl", "name" => "Tagalog"], ["code" => "tl-PH", "name" => "Tagalog (Philippines)"], ["code" => "tg", "name" => "Tajik"], ["code" => "tg-TJ", "name" => "Tajik (Tajikistan)"], ["code" => "tzl", "name" => "Talossan"], ["code" => "ta", "name" => "Tamil"], ["code" => "ta-IN", "name" => "Tamil (India)"], ["code" => "ta-LK", "name" => "Tamil (Sri-Lanka)"], ["code" => "tt", "name" => "Tatar"], ["code" => "te", "name" => "Telugu"], ["code" => "te-IN", "name" => "Telugu (India)"], ["code" => "tet", "name" => "Tetum (Tetun)"], ["code" => "th", "name" => "Thai"], ["code" => "th-TH", "name" => "Thai (Thailand)"], ["code" => "bo", "name" => "Tibetan"], ["code" => "bo-CN", "name" => "Tibetan (China)"], ["code" => "ti", "name" => "Tigrinya"], ["code" => "to", "name" => "Tongan"], ["code" => "ts", "name" => "Tsonga"], ["code" => "tn", "name" => "Tswana"], ["code" => "tr", "name" => "Turkish"], ["code" => "tr-TR", "name" => "Turkish (Turkey)"], ["code" => "tk", "name" => "Turkmen"], ["code" => "tk-TM", "name" => "Turkmen (Turkmenistan)"], ["code" => "udm", "name" => "Udmurt"], ["code" => "ug", "name" => "Uighur"], ["code" => "ug@Arab", "name" => "Uighur (Arabic)"], ["code" => "ug@Cyrl", "name" => "Uighur (Cyrillic)"], ["code" => "ug@Latin", "name" => "Uighur (Latin)"], ["code" => "uk", "name" => "Ukrainian"], ["code" => "uk-UA", "name" => "Ukrainian (Ukraine)"], ["code" => "vmf", "name" => "Upper Franconian"], ["code" => "hsb", "name" => "Upper Sorbian"], ["code" => "ur", "name" => "Urdu"], ["code" => "ur-PK", "name" => "Urdu (Pakistan)"], ["code" => "uz", "name" => "Uzbek"], ["code" => "uz@Arab", "name" => "Uzbek (Arabic)"], ["code" => "uz@Cyrl", "name" => "Uzbek (Cyrillic)"], ["code" => "uz@Latn", "name" => "Uzbek (Latin)"], ["code" => "uz-UZ", "name" => "Uzbek (Uzbekistan)"], ["code" => "ve", "name" => "Venda"], ["code" => "vec", "name" => "Venetian"], ["code" => "vi", "name" => "Vietnamese"], ["code" => "vi-VN", "name" => "Vietnamese (Viet Nam)"], ["code" => "vls", "name" => "Vlaams"], ["code" => "wa", "name" => "Walloon"], ["code" => "war", "name" => "Wáray-Wáray"], ["code" => "cy", "name" => "Welsh"], ["code" => "cy-GB", "name" => "Welsh (United Kingdom)"], ["code" => "fy", "name" => "Western Frisian"], ["code" => "fy-NL", "name" => "Western Frisian (Netherlands)"], ["code" => "wo", "name" => "Wolof"], ["code" => "wo-SN", "name" => "Wolof (Senegal)"], ["code" => "xh", "name" => "Xhosa"], ["code" => "yi", "name" => "Yiddish"], ["code" => "yo", "name" => "Yoruba"], ["code" => "zu", "name" => "Zulu"], ["code" => "zu-ZA", "name" => "Zulu (South Africa)"] + ]; + return response()->json(['results' => $languages]); + } +} diff --git a/v4/routes/web.php b/v4/routes/web.php index 1c86136d09..d27b391748 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -26,4 +26,11 @@ ], function () use ($router) { $router->post('/', 'SurveyController@store'); }); + + // Restricted access + $router->group([ + 'prefix' => '', + ], function () use ($router) { + $router->get('/languages', 'LanguagesController@index'); + }); }); From f45958de6c6703e67ca0a52754911117f026e84c Mon Sep 17 00:00:00 2001 From: Romina Date: Fri, 8 May 2020 09:51:26 -0300 Subject: [PATCH 14/39] Change stages to tasks, attributes to fields --- v4/Http/Controllers/SurveyController.php | 48 ++++++++++++------------ v4/Models/Stage.php | 4 +- v4/Models/Survey.php | 4 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index e46e32a632..c84f785d22 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -47,30 +47,30 @@ protected static function getRules() { 'targeted_survey' => [ Rule::in([false]), ], - 'stages.*.label' => [ + 'tasks.*.label' => [ 'required', 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT ], - 'stages.*.type' => [ + 'tasks.*.type' => [ Rule::in(['post', 'task']) ], - 'stages.*.priority' => [ + 'tasks.*.priority' => [ 'numeric', ], - 'stages.*.icon' => [ + 'tasks.*.icon' => [ 'alpha', ], - 'stages.*.attributes.*.label' => [ + 'tasks.*.fields.*.label' => [ 'required', 'max:150' ], - 'stages.*.attributes.*.key' => [ + 'tasks.*.fields.*.key' => [ 'max:150', 'alpha_dash' // @TODO: add this validation for keys //[[$this->repo, 'isKeyAvailable'], [':value']] ], - 'stages.*.attributes.*.input' => [ + 'tasks.*.fields.*.input' => [ 'required', Rule::in([ 'text', @@ -90,7 +90,7 @@ protected static function getRules() { 'tags', ]) ], - 'stages.*.attributes.*.type' => [ + 'tasks.*.fields.*.type' => [ 'required', Rule::in([ 'decimal', @@ -111,16 +111,16 @@ protected static function getRules() { // @TODO: add this validation for duplicates in type? //[[$this, 'checkForDuplicates'], [':validation', ':value']], ], - 'stages.*.attributes.*.type' => [ + 'tasks.*.fields.*.type' => [ 'boolean' ], - 'stages.*.attributes.*.priority' => [ + 'tasks.*.fields.*.priority' => [ 'numeric', ], - 'stages.*.attributes.*.cardinality' => [ + 'tasks.*.fields.*.cardinality' => [ 'numeric', ], - 'stages.*.attributes.*.response_private' => [ + 'tasks.*.fields.*.response_private' => [ 'boolean' // @TODO add this custom validator for canMakePrivate // [[$this, 'canMakePrivate'], [':value', $type]] @@ -187,17 +187,17 @@ public function store(Request $request) { $request->input(),[ 'updated' => time(), 'created' => time()] ) ); - if ($request->input('stages')) { - foreach ($request->input('stages') as $stage) { - $stage_model = $survey->stages()->create( + if ($request->input('tasks')) { + foreach ($request->input('tasks') as $stage) { + $stage_model = $survey->tasks()->create( array_merge( $stage, [ 'updated' => time(), 'created' => time()] ) ); - foreach ($stage['attributes'] as $attribute) { + foreach ($stage['fields'] as $attribute) { $uuid = Uuid::uuid4(); $attribute['key'] = $uuid->toString(); - $stage_model->attributes()->create( + $stage_model->fields()->create( array_merge( $attribute, [ 'updated' => time(), 'created' => time()] ) @@ -205,7 +205,7 @@ public function store(Request $request) { } } } - return response()->json(['result' => $survey->load('stages')]); + return response()->json(['result' => $survey->load('tasks')]); } /** @@ -226,17 +226,17 @@ public function update(int $id, Request $request) { $request->input(),[ 'updated' => time(), 'created' => time()] ) ); - if ($request->input('stages')) { - foreach ($request->input('stages') as $stage) { - $stage_model = $survey->stages()->create( + if ($request->input('tasks')) { + foreach ($request->input('tasks') as $stage) { + $stage_model = $survey->tasks()->create( array_merge( $stage, [ 'updated' => time(), 'created' => time()] ) ); - foreach ($stage['attributes'] as $attribute) { + foreach ($stage['fields'] as $attribute) { $uuid = Uuid::uuid4(); $attribute['key'] = $uuid->toString(); - $stage_model->attributes()->create( + $stage_model->fields()->create( array_merge( $attribute, [ 'updated' => time(), 'created' => time()] ) @@ -244,6 +244,6 @@ public function update(int $id, Request $request) { } } } - return response()->json(['result' => $survey->load('stages')]); + return response()->json(['result' => $survey->load('tasks')]); } } diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index baa5aa29f8..43aad7655b 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -15,7 +15,7 @@ class Stage extends Model * @var array */ protected $dates = ['created', 'updated']; - protected $with = ['attributes', 'translations']; + protected $with = ['fields', 'translations']; /** * The attributes that are mass assignable. * @@ -33,7 +33,7 @@ class Stage extends Model 'task_is_internal_only' ]; - public function attributes() + public function fields() { return $this->hasMany('v4\Models\Attribute', 'form_stage_id'); } diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 9e93314812..a8ba6d024c 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -11,7 +11,7 @@ class Survey extends Model { use InteractsWithFormPermissions; protected $table = 'forms'; - protected $with = ['stages']; + protected $with = ['tasks']; public $timestamps = FALSE; /** * The attributes that should be hidden for serialization. @@ -99,7 +99,7 @@ private function getCanCreateRoles($form_id) { * We check for relationship permissions here, to avoid hydrating anything that should not be hydrated. * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function stages() + public function tasks() { $authorizer = service('authorizer.form'); $user = $authorizer->getUser(); From 13dabbfa6edcf240e929d0e1072634f300f6fa4a Mon Sep 17 00:00:00 2001 From: Romina Date: Fri, 8 May 2020 21:57:00 -0300 Subject: [PATCH 15/39] Add tests for translations. Save translations with the correct format for all entities --- tests/integration/v4/acl.v4.feature | 58 +++---- tests/integration/v4/translations.v4.feature | 138 +++++++++-------- v4/Http/Controllers/SurveyController.php | 152 ++++--------------- v4/Models/Survey.php | 122 ++++++++++++++- v4/Models/Translation.php | 6 +- v4/Providers/MorphServiceProvider.php | 4 +- 6 files changed, 251 insertions(+), 229 deletions(-) diff --git a/tests/integration/v4/acl.v4.feature b/tests/integration/v4/acl.v4.feature index 38a50e0a52..5c3850fb3a 100644 --- a/tests/integration/v4/acl.v4.feature +++ b/tests/integration/v4/acl.v4.feature @@ -1,53 +1,53 @@ @acl Feature: V4 API Access Control Layer - Scenario: Listing All Stages for all forms with hidden stages + Scenario: Listing All Stages for all forms with hidden tasks Given that I want to get all "Surveys" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" When I request "/surveys" Then the response is JSON - And the response has a "results.7.stages" property - And the "results.7.stages" property count is "1" - And the "results.7.stages.0.attributes" property count is "0" + And the response has a "results.7.tasks" property + And the "results.7.tasks" property count is "1" + And the "results.7.tasks.0.fields" property count is "0" Then the guzzle status code should be 200 - Scenario: Listing All attributes for a stage in an array of forms with a multi-stage form + Scenario: Listing All fields for a stage in an array of forms with a multi-stage form Given that I want to get all "Surveys" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" When I request "/surveys" Then the response is JSON - And the response has a "results.0.stages" property - And the "results.0.stages" property count is "3" - And the "results.0.stages.0.attributes" property count is "17" + And the response has a "results.0.tasks" property + And the "results.0.tasks" property count is "3" + And the "results.0.tasks.0.fields" property count is "17" Then the guzzle status code should be 200 - Scenario: Listing All Stages for a form in an array of forms with hidden stages as admin + Scenario: Listing All Stages for a form in an array of forms with hidden tasks as admin Given that I want to get all "Surveys" And that the oauth token is "testadminuser" And that the api_url is "api/v4" When I request "/surveys" Then the response is JSON - And the response has a "results.7.stages" property - And the "results.7.stages" property count is "4" - And the "results.7.stages.0.attributes" property count is "2" - Scenario: Listing All Stages for a form with hidden stages as admin + And the response has a "results.7.tasks" property + And the "results.7.tasks" property count is "4" + And the "results.7.tasks.0.fields" property count is "2" + Scenario: Listing All Stages for a form with hidden tasks as admin Given that I want to find a "Survey" And that the oauth token is "testadminuser" And that the api_url is "api/v4" When I request "/surveys/8" Then the response is JSON - And the response has a "result.stages" property - And the "result.stages" property count is "4" - And the "result.stages.0.attributes" property count is "2" - And the "result.stages.2.attributes" property count is "0" - Scenario: Listing All Stages for a form with hidden stages as a normal user + And the response has a "result.tasks" property + And the "result.tasks" property count is "4" + And the "result.tasks.0.fields" property count is "2" + And the "result.tasks.2.fields" property count is "0" + Scenario: Listing All Stages for a form with hidden tasks as a normal user Given that I want to find a "Survey" And that the oauth token is "testbasicuser" And that the api_url is "api/v4" When I request "/surveys/8" Then the response is JSON - And the response has a "result.stages" property - And the "result.stages" property count is "1" - And the "result.stages.0.attributes" property count is "0" + And the response has a "result.tasks" property + And the "result.tasks" property count is "1" + And the "result.tasks.0.fields" property count is "0" @rolesEnabled Scenario: User with Manage Settings permission can create a hydrated form Given that I want to make a new "Survey" @@ -63,7 +63,7 @@ Feature: V4 API Access Control Layer "require_approval": true, "everyone_can_create": false, "targeted_survey": false, - "stages": [ + "tasks": [ { "label": "Post", "priority": 0, @@ -71,7 +71,7 @@ Feature: V4 API Access Control Layer "type": "post", "show_when_published": true, "task_is_internal_only": false, - "attributes": [ + "fields": [ { "cardinality": 0, "input": "text", @@ -104,10 +104,10 @@ Feature: V4 API Access Control Layer And the response has a "result" property And the response has a "result.id" property And the type of the "result.id" property is "numeric" - And the response has a "result.stages" property - And the type of the "result.stages" property is "array" - And the response has a "result.stages.0.attributes" property - And the "result.stages.0.attributes" property count is "2" + And the response has a "result.tasks" property + And the type of the "result.tasks" property is "array" + And the response has a "result.tasks.0.fields" property + And the "result.tasks.0.fields" property count is "2" And the "result.name" property equals "new" Then the guzzle status code should be 200 @rolesEnabled @@ -125,7 +125,7 @@ Feature: V4 API Access Control Layer "require_approval": true, "everyone_can_create": false, "targeted_survey": false, - "stages": [ + "tasks": [ { "label": "Post", "priority": 0, @@ -133,7 +133,7 @@ Feature: V4 API Access Control Layer "type": "post", "show_when_published": true, "task_is_internal_only": false, - "attributes": [ + "fields": [ { "cardinality": 0, "input": "text", diff --git a/tests/integration/v4/translations.v4.feature b/tests/integration/v4/translations.v4.feature index 2af79ca877..b1c83cc879 100644 --- a/tests/integration/v4/translations.v4.feature +++ b/tests/integration/v4/translations.v4.feature @@ -1,4 +1,5 @@ @acl +Feature: Testing translations @rolesEnabled Scenario: User with Manage Settings permission can create a hydrated form Given that I want to make a new "Survey" @@ -6,91 +7,88 @@ And that the api_url is "api/v4" And that the request "data" is: """ + { + "enabled_languages": { + "default": "en-EN", + "available": ["es"] + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + "translations": { + "es": { + "name": "nombre" + } + }, + "tasks": [ { - "enabled_languages": { - "default": "en-EN", - "available": ["es"] - }, - "color": null, - "require_approval": true, - "everyone_can_create": false, - "targeted_survey": false, - 'translations': { - "es": - { - "name": "nombre", - } - ] + "translations": { + "es": { + "label": "Reporte", + "description": "Una descripcion" + } }, - // TODO group translations by language - "stages": [ + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "fields": [ { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {}, "translations": { "es": { - "label": "Reporte", - "description": "Una descripcion" + "label": "Titulo", + "instructions": "Instrucciones", + "default": "Un valor por defecto", + "options": ["Una opcion", "otra opcion"] } - }, - "label": "Post", - "priority": 0, - "required": false, - "type": "post", - "show_when_published": true, - "task_is_internal_only": false, - "attributes": [ - { - "cardinality": 0, - "input": "text", - "label": "Title", - "priority": 1, - "required": true, - "type": "title", - "options": [], - "config": {}, - "translations": { - "es": { - "label": "Titulo", - "instructions": "Instrucciones", - "default": "Un valor por defecto", - "options": ["Una opcion", "otra opcion"] - } - }, - }, - { - "cardinality": 0, - "input": "text", - "label": "Description", - "priority": 2, - "required": true, - "type": "description", - "options": [], - "config": {}, - "translations": { - "es": { - "label": "Descripcion", - "instructions": "Instrucciones de la desc", - "default": "Un valor por defecto para desc", - "options": ["Una opcion", "otra opcion desc"] - } - }, + } + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {}, + "translations": { + "es": { + "label": "Descripcion", + "instructions": "Instrucciones de la desc", + "default": "Un valor por defecto para desc", + "options": ["Una opcion", "otra opcion desc"] } - ], - "is_public": true + } } ], - "name": "new" + "is_public": true } - """ + ], + "name": "new" + } + """ When I request "/surveys" Then the response is JSON And the response has a "result" property And the response has a "result.id" property And the type of the "result.id" property is "numeric" - And the response has a "result.stages" property - And the type of the "result.stages" property is "array" - And the response has a "result.stages.0.attributes" property - And the "result.stages.0.attributes" property count is "2" + And the response has a "result.tasks" property + And the type of the "result.tasks" property is "array" + And the response has a "result.tasks.0.fields" property + And the "result.tasks.0.fields" property count is "2" And the "result.translations.es.0.name" property equals "label" - And the "result.stages.0.translations.es.label" property equals "Reporte" + And the "result.tasks.0.translations.es.label" property equals "Reporte" And the "result.name" property equals "new" Then the guzzle status code should be 200 diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index c84f785d22..750d0f7f3a 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -9,128 +9,11 @@ use Illuminate\Validation\Validator; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rule; +use v4\Models\Translation; class SurveyController extends V4Controller { - protected static function getRules() { - return [ - 'name' => [ - 'required', - 'min:2', - 'max:255', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT - ], - 'description' => [ - 'string', - 'nullable' - ], - //@TODO find out where this color validator is implemented - //[['color']], - 'color' => [ - 'string', - 'nullable' - ], - 'disabled' => [ - 'boolean' - ], - 'hide_author' => [ - 'boolean' - ], - 'hide_location' => [ - 'boolean' - ], - 'hide_time' => [ - 'boolean' - ], - // @FIXME: disabled targeted survey creation for v4 forms, need to check - 'targeted_survey' => [ - Rule::in([false]), - ], - 'tasks.*.label' => [ - 'required', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT - ], - 'tasks.*.type' => [ - Rule::in(['post', 'task']) - ], - 'tasks.*.priority' => [ - 'numeric', - ], - 'tasks.*.icon' => [ - 'alpha', - ], - 'tasks.*.fields.*.label' => [ - 'required', - 'max:150' - ], - 'tasks.*.fields.*.key' => [ - 'max:150', - 'alpha_dash' - // @TODO: add this validation for keys - //[[$this->repo, 'isKeyAvailable'], [':value']] - ], - 'tasks.*.fields.*.input' => [ - 'required', - Rule::in([ - 'text', - 'textarea', - 'select', - 'radio', - 'checkbox', - 'checkboxes', - 'date', - 'datetime', - 'location', - 'number', - 'relation', - 'upload', - 'video', - 'markdown', - 'tags', - ]) - ], - 'tasks.*.fields.*.type' => [ - 'required', - Rule::in([ - 'decimal', - 'int', - 'geometry', - 'text', - 'varchar', - 'markdown', - 'point', - 'datetime', - 'link', - 'relation', - 'media', - 'title', - 'description', - 'tags', - ]) - // @TODO: add this validation for duplicates in type? - //[[$this, 'checkForDuplicates'], [':validation', ':value']], - ], - 'tasks.*.fields.*.type' => [ - 'boolean' - ], - 'tasks.*.fields.*.priority' => [ - 'numeric', - ], - 'tasks.*.fields.*.cardinality' => [ - 'numeric', - ], - 'tasks.*.fields.*.response_private' => [ - 'boolean' - // @TODO add this custom validator for canMakePrivate - // [[$this, 'canMakePrivate'], [':value', $type]] - ] - // @NOTE: checkPostTypeLimit is not used here. - // Before merge, validate with Angela if we - // should be removing that arbitrary limit since it's pretty rare - // for it to be needed - ]; - } /** * Display the specified resource. * @@ -159,7 +42,6 @@ public function show(int $id) /** * Display the specified resource. - * @TODO add translation keys to each object =) * @TODO add enabled_languages (the ones that we have translations for) * @return \Illuminate\Http\JsonResponse * @throws \Illuminate\Auth\Access\AuthorizationException @@ -172,7 +54,6 @@ public function index() /** * Display the specified resource. - * @TODO add translation keys to each object =) * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param Request $request @@ -181,12 +62,14 @@ public function index() */ public function store(Request $request) { $this->authorize('store', Survey::class); - $this->getValidationFactory()->make($request->input(), self::getRules()); + $input = $request->all(); + $this->getValidationFactory()->make($request->input(), Survey::getRules()); $survey = Survey::create( array_merge( $request->input(),[ 'updated' => time(), 'created' => time()] ) ); + $this->saveTranslations($request->input('translations'), $survey->id, 'survey'); if ($request->input('tasks')) { foreach ($request->input('tasks') as $stage) { $stage_model = $survey->tasks()->create( @@ -194,25 +77,48 @@ public function store(Request $request) { $stage, [ 'updated' => time(), 'created' => time()] ) ); + $this->saveTranslations($stage['translations'], $stage_model->id, 'task'); foreach ($stage['fields'] as $attribute) { $uuid = Uuid::uuid4(); $attribute['key'] = $uuid->toString(); - $stage_model->fields()->create( + $field_model = $stage_model->fields()->create( array_merge( $attribute, [ 'updated' => time(), 'created' => time()] ) ); + $this->saveTranslations($attribute['translations'], $field_model->id, 'field'); + } } } return response()->json(['result' => $survey->load('tasks')]); } + private function saveTranslations($input, $translatable_id, $type) { + if (!is_array($input)) { + return true; + } + foreach ($input as $language => $translations) { + foreach ($translations as $key => $translated) { + if (is_array($translated)){ + $translated = json_encode($translated); + } + Translation::create([ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language + ]); + } + } + } /** * Display the specified resource. * @TODO add translation keys to each object =) * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) + * @param int $id * @param Request $request * @return \Illuminate\Http\JsonResponse * @throws \Illuminate\Auth\Access\AuthorizationException @@ -220,7 +126,7 @@ public function store(Request $request) { public function update(int $id, Request $request) { $survey = Survey::find($id); $this->authorize('update', $survey); - $this->getValidationFactory()->make($request->input(), self::getRules()); + $this->getValidationFactory()->make($request->input(), Survey::getRules()); $survey = Survey::create( array_merge( $request->input(),[ 'updated' => time(), 'created' => time()] diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index a8ba6d024c..3a1dbed0f2 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -3,7 +3,9 @@ namespace v4\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Validation\Rule; use Ushahidi\App\Repository\FormRepository; +use Ushahidi\App\Validator\LegacyValidator; use Ushahidi\Core\Entity\Permission; use Ushahidi\Core\Tool\Permissions\InteractsWithFormPermissions; @@ -114,8 +116,124 @@ public function tasks() ->where('form_stages.task_is_internal_only', '=', '0'); } - - + protected static function getRules() { + return [ + 'name' => [ + 'required', + 'min:2', + 'max:255', + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT + ], + 'description' => [ + 'string', + 'nullable' + ], + //@TODO find out where this color validator is implemented + //[['color']], + 'color' => [ + 'string', + 'nullable' + ], + 'disabled' => [ + 'boolean' + ], + 'hide_author' => [ + 'boolean' + ], + 'hide_location' => [ + 'boolean' + ], + 'hide_time' => [ + 'boolean' + ], + // @FIXME: disabled targeted survey creation for v4 forms, need to check + 'targeted_survey' => [ + Rule::in([false]), + ], + 'tasks.*.label' => [ + 'required', + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT + ], + 'tasks.*.type' => [ + Rule::in(['post', 'task']) + ], + 'tasks.*.priority' => [ + 'numeric', + ], + 'tasks.*.icon' => [ + 'alpha', + ], + 'tasks.*.fields.*.label' => [ + 'required', + 'max:150' + ], + 'tasks.*.fields.*.key' => [ + 'max:150', + 'alpha_dash' + // @TODO: add this validation for keys + //[[$this->repo, 'isKeyAvailable'], [':value']] + ], + 'tasks.*.fields.*.input' => [ + 'required', + Rule::in([ + 'text', + 'textarea', + 'select', + 'radio', + 'checkbox', + 'checkboxes', + 'date', + 'datetime', + 'location', + 'number', + 'relation', + 'upload', + 'video', + 'markdown', + 'tags', + ]) + ], + 'tasks.*.fields.*.type' => [ + 'required', + Rule::in([ + 'decimal', + 'int', + 'geometry', + 'text', + 'varchar', + 'markdown', + 'point', + 'datetime', + 'link', + 'relation', + 'media', + 'title', + 'description', + 'tags', + ]) + // @TODO: add this validation for duplicates in type? + //[[$this, 'checkForDuplicates'], [':validation', ':value']], + ], + 'tasks.*.fields.*.type' => [ + 'boolean' + ], + 'tasks.*.fields.*.priority' => [ + 'numeric', + ], + 'tasks.*.fields.*.cardinality' => [ + 'numeric', + ], + 'tasks.*.fields.*.response_private' => [ + 'boolean' + // @TODO add this custom validator for canMakePrivate + // [[$this, 'canMakePrivate'], [':value', $type]] + ] + // @NOTE: checkPostTypeLimit is not used here. + // Before merge, validate with Angela if we + // should be removing that arbitrary limit since it's pretty rare + // for it to be needed + ]; + } /** * Get the survey's translation. diff --git a/v4/Models/Translation.php b/v4/Models/Translation.php index 02f2e1aea6..ad60f53f3b 100644 --- a/v4/Models/Translation.php +++ b/v4/Models/Translation.php @@ -21,15 +21,15 @@ class Translation extends Model * @var array */ protected $fillable = [ - 'entity_type', - 'entity_id', + 'translatable_id', 'translated_key', + 'translatable_type', 'translation', 'language', ]; /** - * Get the owning imageable model. + * Get the owning translatable model. */ public function translatable() { diff --git a/v4/Providers/MorphServiceProvider.php b/v4/Providers/MorphServiceProvider.php index 46107ac42f..a8a20b00c3 100644 --- a/v4/Providers/MorphServiceProvider.php +++ b/v4/Providers/MorphServiceProvider.php @@ -11,8 +11,8 @@ class MorphServiceProvider extends ServiceProvider public function boot() { Relation::morphMap([ 'survey' => 'v4\Models\Survey', - 'stage' => 'v4\Models\Stage', - 'attribute' => 'v4\Models\Attribute', + 'task' => 'v4\Models\Stage', + 'field' => 'v4\Models\Attribute', ]); } } From 45b8b17ef0b2c8f71792f841ddd0c2a3b167b8b1 Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 00:31:47 -0300 Subject: [PATCH 16/39] Add resources to properly format results and hydrate translations with a better format --- tests/integration/v4/translations.v4.feature | 8 +++-- v4/Http/Controllers/SurveyController.php | 19 ++++++----- v4/Http/Resources/FieldCollection.php | 27 +++++++++++++++ v4/Http/Resources/FieldResource.php | 34 ++++++++++++++++++ v4/Http/Resources/SurveyResource.php | 35 +++++++++++++++++++ v4/Http/Resources/TaskCollection.php | 27 +++++++++++++++ v4/Http/Resources/TaskResource.php | 32 +++++++++++++++++ v4/Http/Resources/TranslationCollection.php | 36 ++++++++++++++++++++ v4/Http/Resources/TranslationResource.php | 24 +++++++++++++ v4/Models/Survey.php | 8 ----- 10 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 v4/Http/Resources/FieldCollection.php create mode 100644 v4/Http/Resources/FieldResource.php create mode 100644 v4/Http/Resources/SurveyResource.php create mode 100644 v4/Http/Resources/TaskCollection.php create mode 100644 v4/Http/Resources/TaskResource.php create mode 100644 v4/Http/Resources/TranslationCollection.php create mode 100644 v4/Http/Resources/TranslationResource.php diff --git a/tests/integration/v4/translations.v4.feature b/tests/integration/v4/translations.v4.feature index b1c83cc879..41315120da 100644 --- a/tests/integration/v4/translations.v4.feature +++ b/tests/integration/v4/translations.v4.feature @@ -18,7 +18,7 @@ Feature: Testing translations "targeted_survey": false, "translations": { "es": { - "name": "nombre" + "name": "nombre" } }, "tasks": [ @@ -88,7 +88,9 @@ Feature: Testing translations And the type of the "result.tasks" property is "array" And the response has a "result.tasks.0.fields" property And the "result.tasks.0.fields" property count is "2" - And the "result.translations.es.0.name" property equals "label" + And the "result.translations.es.name" property equals "nombre" + And the "result.tasks.0.translations.es.description" property equals "Una descripcion" And the "result.tasks.0.translations.es.label" property equals "Reporte" + And the "result.tasks.0.fields.0.translations.es.instructions" property equals "Instrucciones" And the "result.name" property equals "new" - Then the guzzle status code should be 200 + Then the guzzle status code should be 201 diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 750d0f7f3a..a69d87e13e 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -1,19 +1,18 @@ json(['result' => $survey]); + return new \v4\Http\Resources\SurveyResource($survey); } /** @@ -62,7 +61,6 @@ public function index() */ public function store(Request $request) { $this->authorize('store', Survey::class); - $input = $request->all(); $this->getValidationFactory()->make($request->input(), Survey::getRules()); $survey = Survey::create( array_merge( @@ -91,10 +89,16 @@ public function store(Request $request) { } } } - return response()->json(['result' => $survey->load('tasks')]); + return new \v4\Http\Resources\SurveyResource($survey); } - private function saveTranslations($input, $translatable_id, $type) { + /** + * @param $input + * @param $translatable_id + * @param $type + * @return bool + */ + private function saveTranslations($input, int $translatable_id, string $type) { if (!is_array($input)) { return true; } @@ -115,7 +119,6 @@ private function saveTranslations($input, $translatable_id, $type) { } /** * Display the specified resource. - * @TODO add translation keys to each object =) * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param int $id diff --git a/v4/Http/Resources/FieldCollection.php b/v4/Http/Resources/FieldCollection.php new file mode 100644 index 0000000000..c679e83968 --- /dev/null +++ b/v4/Http/Resources/FieldCollection.php @@ -0,0 +1,27 @@ +collection; + } +} diff --git a/v4/Http/Resources/FieldResource.php b/v4/Http/Resources/FieldResource.php new file mode 100644 index 0000000000..3e84660f9b --- /dev/null +++ b/v4/Http/Resources/FieldResource.php @@ -0,0 +1,34 @@ + $this->key, + 'label' => $this->label, + 'instructions' => $this->instructions, + 'input' => $this->input, + 'type' => $this->type, + 'required' => $this->required, + 'default' => $this->default, + 'priority' => $this->priority, + 'options' => $this->options, + 'cardinality' => $this->cardinality, + 'config' => $this->config, + 'response_private' => $this->response_private, + 'form_stage_id' => $this->form_stage_id, + 'translations' => new TranslationCollection($this->translations), + ]; + } +} diff --git a/v4/Http/Resources/SurveyResource.php b/v4/Http/Resources/SurveyResource.php new file mode 100644 index 0000000000..77bdfe5981 --- /dev/null +++ b/v4/Http/Resources/SurveyResource.php @@ -0,0 +1,35 @@ + $this->id, + 'name' => $this->name, + 'parent_id' => $this->parent_id, + 'description' => $this->description, + 'type' => $this->type, + 'disabled' => $this->disabled, + 'require_approval' => $this->require_approval, + 'everyone_can_create' => $this->everyone_can_create, + 'color' => $this->color, + 'hide_author' => $this->hide_author, + 'hide_time' => $this->hide_time, + 'hide_location' => $this->hide_location, + 'targeted_survey' => $this->targeted_survey, + 'translations' => new TranslationCollection($this->translations), + 'tasks' => new TaskCollection($this->tasks) + ]; + } +} diff --git a/v4/Http/Resources/TaskCollection.php b/v4/Http/Resources/TaskCollection.php new file mode 100644 index 0000000000..5a2cd1070a --- /dev/null +++ b/v4/Http/Resources/TaskCollection.php @@ -0,0 +1,27 @@ +collection; + } +} diff --git a/v4/Http/Resources/TaskResource.php b/v4/Http/Resources/TaskResource.php new file mode 100644 index 0000000000..c0157a8917 --- /dev/null +++ b/v4/Http/Resources/TaskResource.php @@ -0,0 +1,32 @@ + $this->form_id, + 'label' => $this->label, + 'priority' => $this->priority, + 'icon' => $this->icon, + 'required' => $this->required, + 'type' => $this->type, + 'description' => $this->description, + 'show_when_published' => $this->show_when_published, + 'task_is_internal_only' => $this->task_is_internal_only, + 'id' => $this->id, + 'fields' => new FieldCollection($this->fields), + 'translations' => new TranslationCollection($this->translations) + ]; + } +} diff --git a/v4/Http/Resources/TranslationCollection.php b/v4/Http/Resources/TranslationCollection.php new file mode 100644 index 0000000000..1332b5901e --- /dev/null +++ b/v4/Http/Resources/TranslationCollection.php @@ -0,0 +1,36 @@ +collection->mapToGroups(function ($item, $key) { + return [$item->language => $item]; + }); + $combined = $grouped->map(function ($item, $key) { + return $item->mapWithKeys(function($i) { + return [$i->translated_key => $i->translation ]; + }); + }); + return $combined; + } +} diff --git a/v4/Http/Resources/TranslationResource.php b/v4/Http/Resources/TranslationResource.php new file mode 100644 index 0000000000..c3d561a8d2 --- /dev/null +++ b/v4/Http/Resources/TranslationResource.php @@ -0,0 +1,24 @@ + $this->translated_key, + 'translation' => $this->translation, + 'language' => $this->language + + ]; + } +} diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 3a1dbed0f2..6408e5b75a 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -78,14 +78,6 @@ public function getCanCreateAttribute() { $can_create = $this->getCanCreateRoles($this->id); return $can_create['roles']; } -// -// /** -// * This is what makes can_create possible -// * @return mixed -// */ -// public function getTranslationsAttribute() { -// return $this->translations; -// } private function getCanCreateRoles($form_id) { /** From 6341324397f1612a74ad850d54503215b370df26 Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 00:43:25 -0300 Subject: [PATCH 17/39] Add resources correctly to GET / and POST with and without translation keys --- tests/integration/v4/acl.v4.feature | 2 +- tests/integration/v4/forms/forms.v4.feature | 2 +- v4/Http/Controllers/SurveyController.php | 6 ++--- v4/Http/Resources/SurveyCollection.php | 29 +++++++++++++++++++++ v4/Http/Resources/SurveyResource.php | 3 ++- 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 v4/Http/Resources/SurveyCollection.php diff --git a/tests/integration/v4/acl.v4.feature b/tests/integration/v4/acl.v4.feature index 5c3850fb3a..857cc28ff0 100644 --- a/tests/integration/v4/acl.v4.feature +++ b/tests/integration/v4/acl.v4.feature @@ -109,7 +109,7 @@ Feature: V4 API Access Control Layer And the response has a "result.tasks.0.fields" property And the "result.tasks.0.fields" property count is "2" And the "result.name" property equals "new" - Then the guzzle status code should be 200 + Then the guzzle status code should be 201 @rolesEnabled Scenario: Basic user CANNOT create a hydrated form Given that I want to make a new "Survey" diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index feca00fd89..d52a4c372d 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -25,7 +25,7 @@ Feature: Testing the Surveys API And the "result.everyone_can_create" property is true And the response has a "result.can_create" property And the "result.can_create" property is empty - Then the guzzle status code should be 200 + Then the guzzle status code should be 201 # # Scenario: Updating a Survey # Given that I want to update a "Survey" diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index a69d87e13e..1d2d198f06 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -48,7 +48,7 @@ public function show(int $id) public function index() { $this->authorize('index', Survey::class); - return response()->json(['results' => Survey::all()]); + return new \v4\Http\Resources\SurveyCollection(Survey::all()); } /** @@ -75,7 +75,7 @@ public function store(Request $request) { $stage, [ 'updated' => time(), 'created' => time()] ) ); - $this->saveTranslations($stage['translations'], $stage_model->id, 'task'); + $this->saveTranslations($stage['translations'] ?? [], $stage_model->id, 'task'); foreach ($stage['fields'] as $attribute) { $uuid = Uuid::uuid4(); $attribute['key'] = $uuid->toString(); @@ -84,7 +84,7 @@ public function store(Request $request) { $attribute, [ 'updated' => time(), 'created' => time()] ) ); - $this->saveTranslations($attribute['translations'], $field_model->id, 'field'); + $this->saveTranslations($attribute['translations'] ?? [], $field_model->id, 'field'); } } diff --git a/v4/Http/Resources/SurveyCollection.php b/v4/Http/Resources/SurveyCollection.php new file mode 100644 index 0000000000..efddd19c43 --- /dev/null +++ b/v4/Http/Resources/SurveyCollection.php @@ -0,0 +1,29 @@ +collection; + } +} diff --git a/v4/Http/Resources/SurveyResource.php b/v4/Http/Resources/SurveyResource.php index 77bdfe5981..74eb9da0ec 100644 --- a/v4/Http/Resources/SurveyResource.php +++ b/v4/Http/Resources/SurveyResource.php @@ -29,7 +29,8 @@ public function toArray($request) 'hide_location' => $this->hide_location, 'targeted_survey' => $this->targeted_survey, 'translations' => new TranslationCollection($this->translations), - 'tasks' => new TaskCollection($this->tasks) + 'tasks' => new TaskCollection($this->tasks), + 'can_create' => $this->can_create ]; } } From d18962e0051e67709edc225f514de15395aa7c0e Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 01:55:19 -0300 Subject: [PATCH 18/39] Use the correct scopes and don't run policies twice (ie guards then policies) as I was doing before. Use the scopes we already have --- v4/Http/Controllers/SurveyController.php | 19 ------------------- v4/Http/Resources/FieldResource.php | 1 + v4/Http/Resources/TaskResource.php | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 1d2d198f06..6e555cd439 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -1,15 +1,11 @@ find($id); - $not_found = !$survey; - if ($not_found) { - $survey = new Survey(); - } - // we try to authorize even if we don't find a survey - // this allows us to return a 404 to users who would - // be allowed to read surveys and a 403 to those who wouldn't - // obfuscating the existence of particular unauthorized surveys - // or non-existent ones to users without any permissions to see them - $this->authorize('show', $survey); - if ($not_found) { - abort(404); - } return new \v4\Http\Resources\SurveyResource($survey); } @@ -47,7 +30,6 @@ public function show(int $id) */ public function index() { - $this->authorize('index', Survey::class); return new \v4\Http\Resources\SurveyCollection(Survey::all()); } @@ -60,7 +42,6 @@ public function index() * @throws \Illuminate\Auth\Access\AuthorizationException */ public function store(Request $request) { - $this->authorize('store', Survey::class); $this->getValidationFactory()->make($request->input(), Survey::getRules()); $survey = Survey::create( array_merge( diff --git a/v4/Http/Resources/FieldResource.php b/v4/Http/Resources/FieldResource.php index 3e84660f9b..ca70111ed9 100644 --- a/v4/Http/Resources/FieldResource.php +++ b/v4/Http/Resources/FieldResource.php @@ -15,6 +15,7 @@ class FieldResource extends Resource public function toArray($request) { return [ + 'id' => $this->id, 'key' => $this->key, 'label' => $this->label, 'instructions' => $this->instructions, diff --git a/v4/Http/Resources/TaskResource.php b/v4/Http/Resources/TaskResource.php index c0157a8917..a8da3c681e 100644 --- a/v4/Http/Resources/TaskResource.php +++ b/v4/Http/Resources/TaskResource.php @@ -15,6 +15,7 @@ class TaskResource extends Resource public function toArray($request) { return [ + 'id' => $this->id, 'form_id' => $this->form_id, 'label' => $this->label, 'priority' => $this->priority, @@ -24,7 +25,6 @@ public function toArray($request) 'description' => $this->description, 'show_when_published' => $this->show_when_published, 'task_is_internal_only' => $this->task_is_internal_only, - 'id' => $this->id, 'fields' => new FieldCollection($this->fields), 'translations' => new TranslationCollection($this->translations) ]; From 25bbe3ae20f3e225f3f68e2cce8ab70558dc454c Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 02:21:33 -0300 Subject: [PATCH 19/39] If there is a user run the authorizer to check permissions - store --- v4/Http/Controllers/SurveyController.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 6e555cd439..2ad2e66a03 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -1,11 +1,16 @@ find($id); + if (!$survey) { + abort(404); + } return new \v4\Http\Resources\SurveyResource($survey); } @@ -42,6 +50,16 @@ public function index() * @throws \Illuminate\Auth\Access\AuthorizationException */ public function store(Request $request) { + $authorizer = service('authorizer.form'); + // if there's no user the guards will kick them off already, but if there + // is one we need to check the authorizer to ensure we don't let + // users without admin perms create forms etc + // this is an unfortunate problem with using an old version of lumen + // that doesn't let me do guest user checks without adding more risk. + $user = $authorizer->getUser(); + if ($user) { + $this->authorize('store', Survey::class); + } $this->getValidationFactory()->make($request->input(), Survey::getRules()); $survey = Survey::create( array_merge( From 9d014adac9f5b58f837d9a4059360be3f7708dff Mon Sep 17 00:00:00 2001 From: rowasc Date: Sat, 9 May 2020 20:57:28 +0000 Subject: [PATCH 20/39] Add default language to surveys, and return the values in surveyr esponse --- ...20200506131856_add_entity_translations.php | 5 +++- .../20200509204243_add_language_to_survey.php | 23 +++++++++++++++++++ v4/Http/Controllers/SurveyController.php | 14 ++++++----- v4/Http/Resources/SurveyResource.php | 6 ++++- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 migrations/20200509204243_add_language_to_survey.php diff --git a/migrations/20200506131856_add_entity_translations.php b/migrations/20200506131856_add_entity_translations.php index 44b8aa2e89..e16703792d 100644 --- a/migrations/20200506131856_add_entity_translations.php +++ b/migrations/20200506131856_add_entity_translations.php @@ -5,7 +5,7 @@ class AddEntityTranslations extends AbstractMigration { - public function change() + public function up() { $this->table('translations') ->addColumn('translatable_type', 'string', ['null' => false]) //form, attribute,stage,category @@ -16,4 +16,7 @@ public function change() ->addTimestamps() ->create(); } + public function down() { + $this->dropTable('translations'); + } } diff --git a/migrations/20200509204243_add_language_to_survey.php b/migrations/20200509204243_add_language_to_survey.php new file mode 100644 index 0000000000..454b1a3d9a --- /dev/null +++ b/migrations/20200509204243_add_language_to_survey.php @@ -0,0 +1,23 @@ +fetchRow( + "SELECT config_value FROM config WHERE group_name='site' and config_key='language' " + ); + // get two letter lang code + $language = str_before(json_decode($result['config_value']), '-'); + $this->table('forms') + ->addColumn('base_language', 'string', ['null' => false, 'default' => $language]) //es/en + ->update(); + } + + public function down() { + $this->table('forms')->removeColumn('base_language'); + } +} diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 2ad2e66a03..04334349af 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -5,6 +5,8 @@ use Illuminate\Http\Resources\Json\Resource; use Ramsey\Uuid\Uuid; use Ushahidi\App\Validator\LegacyValidator; +use v4\Http\Resources\SurveyCollection; +use v4\Http\Resources\SurveyResource; use v4\Models\Attribute; use v4\Models\Survey; use Illuminate\Http\Request; @@ -18,7 +20,7 @@ class SurveyController extends V4Controller * Display the specified resource. * * @param int $id - * @return \Illuminate\Http\JsonResponse + * @return SurveyResource * @throws \Illuminate\Auth\Access\AuthorizationException */ public function show(int $id) @@ -27,18 +29,18 @@ public function show(int $id) if (!$survey) { abort(404); } - return new \v4\Http\Resources\SurveyResource($survey); + return new SurveyResource($survey); } /** * Display the specified resource. * @TODO add enabled_languages (the ones that we have translations for) - * @return \Illuminate\Http\JsonResponse + * @return SurveyCollection * @throws \Illuminate\Auth\Access\AuthorizationException */ public function index() { - return new \v4\Http\Resources\SurveyCollection(Survey::all()); + return new SurveyCollection(Survey::all()); } /** @@ -46,7 +48,7 @@ public function index() * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param Request $request - * @return \Illuminate\Http\JsonResponse + * @return SurveyResource * @throws \Illuminate\Auth\Access\AuthorizationException */ public function store(Request $request) { @@ -88,7 +90,7 @@ public function store(Request $request) { } } } - return new \v4\Http\Resources\SurveyResource($survey); + return new SurveyResource($survey); } /** diff --git a/v4/Http/Resources/SurveyResource.php b/v4/Http/Resources/SurveyResource.php index 74eb9da0ec..6ff34fa84d 100644 --- a/v4/Http/Resources/SurveyResource.php +++ b/v4/Http/Resources/SurveyResource.php @@ -30,7 +30,11 @@ public function toArray($request) 'targeted_survey' => $this->targeted_survey, 'translations' => new TranslationCollection($this->translations), 'tasks' => new TaskCollection($this->tasks), - 'can_create' => $this->can_create + 'can_create' => $this->can_create, + 'enabled_languages' => [ + 'default'=> $this->base_language, + 'available' => $this->translations->groupBy('language')->keys() + ] ]; } } From ad0333f14dfc88f4b072fd41d0d876da692c69a8 Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 18:01:06 -0300 Subject: [PATCH 21/39] base_language for surveys now available --- v4/Http/Controllers/SurveyController.php | 1 - v4/Models/Survey.php | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 04334349af..27c905f5b3 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -86,7 +86,6 @@ public function store(Request $request) { ) ); $this->saveTranslations($attribute['translations'] ?? [], $field_model->id, 'field'); - } } } diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 6408e5b75a..67d55a2668 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -46,7 +46,8 @@ class Survey extends Model 'hide_author', 'hide_time', 'hide_location', - 'targeted_survey' + 'targeted_survey', + 'base_language' ]; /** From bd1a00d2a629edfa80b8f1cdcb3b8380be76e540 Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 21:33:07 -0300 Subject: [PATCH 22/39] Fix validation rules, add and test update and delete features --- tests/integration/v4/forms/forms.v4.feature | 581 ++++++++++++++++++-- v4/Http/Controllers/SurveyController.php | 149 ++++- v4/Models/Survey.php | 20 +- v4/Policies/SurveyPolicy.php | 14 +- v4/routes/web.php | 2 + 5 files changed, 692 insertions(+), 74 deletions(-) diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index d52a4c372d..a8676060b2 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -27,52 +27,531 @@ Feature: Testing the Surveys API And the "result.can_create" property is empty Then the guzzle status code should be 201 # -# Scenario: Updating a Survey -# Given that I want to update a "Survey" -# And that the api_url is "api/v4" -# And that the request "data" is: -# """ -# { -# "name":"Updated Test Survey", -# "type":"report", -# "description":"This is a test form updated by BDD testing", -# "disabled":true, -# "require_approval":false, -# "everyone_can_create":false, -# "tags": [1,2,3,"junk"] -# } -# """ -# And that its "id" is "1" -# When I request "/surveys" -# Then the response is JSON -# And the response has a "result.id" property -# And the type of the "result.id" property is "numeric" -# And the "result.id" property equals "1" -# And the response has a "result.name" property -# And the "result.name" property equals "Updated Test Survey" -# And the "result.disabled" property is true -# And the "result.require_approval" property is false -# And the "result.everyone_can_create" property is false -# Then the guzzle status code should be 200 + Scenario: Updating a Survey + Given that I want to update a "Survey" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "id": 1, + "name": "Test Form has been updated name", + "parent_id": null, + "description": "Testing form is updated desc", + "type": "report", + "disabled": 0, + "require_approval": 0, + "everyone_can_create": 0, + "color": null, + "hide_author": 0, + "hide_time": 0, + "hide_location": 0, + "targeted_survey": 0, + "base_language": "en", + "translations": { + "es": { + "name": "ES Test Form has been updated name", + "description": "ES Testing form is updated desc" + } + }, + "tasks": [ + { + "id": 1, + "form_id": 1, + "label": "Main task 1 updated", + "priority": 1, + "required": 0, + "type": "post", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 1, + "key": "test_varchar", + "label": "Test varchar", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test varchar" + } + } + }, + { + "id": 2, + "key": "test_point", + "label": "Test point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test point" + } + } + }, + { + "id": 3, + "key": "full_name", + "label": "Full Name", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Full Name" + } + } + }, + { + "id": 4, + "key": "description", + "label": "Description", + "instructions": null, + "input": "textarea", + "type": "description", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Description" + } + } + }, + { + "id": 5, + "key": "date_of_birth", + "label": "Date of birth", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 3, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Date of birth" + } + } + }, + { + "id": 6, + "key": "missing_date", + "label": "Missing date", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 4, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Missing date" + } + } + }, + { + "id": 7, + "key": "last_location", + "label": "Last Location", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 1, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location" + } + } + }, + { + "id": 8, + "key": "last_location_point", + "label": "Last Location (point)", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location (point)" + } + } + }, + { + "id": 9, + "key": "geometry_test", + "label": "Geometry test", + "instructions": null, + "input": "text", + "type": "geometry", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Geometry test" + } + } + }, + { + "id": 10, + "key": "missing_status", + "label": "Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Status" + } + } + }, + { + "id": 11, + "key": "links", + "label": "Links", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Links" + } + } + }, + { + "id": 12, + "key": "second_point", + "label": "Second Point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Second Point" + } + } + }, + { + "id": 14, + "key": "media_test", + "label": "Media Test", + "instructions": null, + "input": "upload", + "type": "media", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Media Test" + } + } + }, + { + "id": 15, + "key": "possible_actions", + "label": "Possible actions", + "instructions": null, + "input": "checkbox", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "ground_search", + "medical_evacuation" + ], + "cardinality": 0, + "config": [], + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Possible actions" + } + } + }, + { + "id": 17, + "key": "title", + "label": "Title", + "instructions": null, + "input": "text", + "type": "title", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Title" + } + } + }, + { + "id": 25, + "key": "markdown", + "label": "Test markdown", + "instructions": null, + "input": "text", + "type": "markdown", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test markdown" + } + } + }, + { + "id": 26, + "key": "tags1", + "label": "Categories", + "instructions": null, + "input": "tags", + "type": "tags", + "required": 0, + "default": null, + "priority": 3, + "options": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Categories", + "options": [ + "ES 1", + "ES 2", + "ES 3", + "ES 4", + "ES 5", + "ES 6", + "ES 7" + ] + } + } + } + ], + "translations": { + "es": { + "label": "ES Main task 1" + } + } + }, + { + "id": 2, + "form_id": 1, + "label": "2nd step", + "priority": 2, + "required": 0, + "type": "task", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 13, + "key": "person_status", + "label": "Person Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 2, + "translations": { + "es": { + "label": "ES Person Status" + } + } + } + ], + "translations": { + "es": { + "label": "ES 2nd step" + } + } + }, + { + "id": 3, + "form_id": 1, + "label": "3rd step", + "priority": 3, + "required": 0, + "type": "task", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [], + "translations": [] + } + ] + } + """ + And that its "id" is "1" + When I request "/surveys" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.id" property equals "1" + And the response has a "result.name" property + And the "result.name" property equals "Test Form has been updated name" + And the "result.disabled" property is false + And the "result.require_approval" property is false + And the "result.everyone_can_create" property is false + And the "result.translations.es.name" property equals "ES Test Form has been updated name" + And the "result.tasks.0.label" property equals "Main task 1 updated" + And the "result.tasks.0.translations.es.label" property equals "ES Main task 1" + And the "result.tasks.0.fields.0.label" property equals "Test varchar" + And the "result.tasks.0.fields.0.translations.es.label" property equals "ES Test varchar" + Then the guzzle status code should be 200 # -# Scenario: Updating a Survey to clear name should fail -# Given that I want to update a "Survey" -# And that the api_url is "api/v4" -# And that the request "data" is: -# """ -# { -# "name":"", -# "type":"report", -# "description":"This is a test form updated by BDD testing", -# "disabled":true, -# "require_approval":false, -# "everyone_can_create":false -# } -# """ -# And that its "id" is "1" -# When I request "/surveys" -# Then the response is JSON -# Then the guzzle status code should be 422 + Scenario: Updating a Survey to clear name should fail + Given that I want to update a "Survey" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "name":"", + "type":"report", + "description":"This is a test form updated by BDD testing", + "disabled":true, + "require_approval":false, + "everyone_can_create":false + } + """ + And that its "id" is "1" + When I request "/surveys" + Then the response is JSON + Then the guzzle status code should be 422 # # Scenario: Update a non-existent Survey # Given that I want to update a "Survey" @@ -110,10 +589,20 @@ Feature: Testing the Surveys API And the type of the "result.id" property is "numeric" Then the guzzle status code should be 200 + Scenario: Deleting a Survey + Given that I want to delete a "Survey" + And that its "id" is "1" + And that the api_url is "api/v4" + When I request "/surveys" + Then the response is JSON + And the response has a "result.deleted" property + And the type of the "result.deleted" property is "numeric" + And the "result.deleted" property equals "1" + Then the guzzle status code should be 200 Scenario: Finding a non-existent Survey Given that I want to find a "Survey" And that the api_url is "api/v4" - And that its "id" is "35" + And that its "id" is "1332" When I request "/surveys" Then the response is JSON And the response has a "errors" property diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 27c905f5b3..2eb2583ff8 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -3,11 +3,13 @@ namespace v4\Http\Controllers; use Illuminate\Auth\Access\Gate; use Illuminate\Http\Resources\Json\Resource; +use Illuminate\Support\Collection; use Ramsey\Uuid\Uuid; use Ushahidi\App\Validator\LegacyValidator; use v4\Http\Resources\SurveyCollection; use v4\Http\Resources\SurveyResource; use v4\Models\Attribute; +use v4\Models\Stage; use v4\Models\Survey; use Illuminate\Http\Request; use v4\Models\Translation; @@ -20,14 +22,18 @@ class SurveyController extends V4Controller * Display the specified resource. * * @param int $id - * @return SurveyResource + * @return mixed * @throws \Illuminate\Auth\Access\AuthorizationException */ public function show(int $id) { $survey = Survey::with('translations')->find($id); if (!$survey) { - abort(404); + return response()->json( + [ + 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + ], + 404); } return new SurveyResource($survey); } @@ -62,7 +68,7 @@ public function store(Request $request) { if ($user) { $this->authorize('store', Survey::class); } - $this->getValidationFactory()->make($request->input(), Survey::getRules()); + $this->validate($request, Survey::getRules()); $survey = Survey::create( array_merge( $request->input(),[ 'updated' => time(), 'created' => time()] @@ -117,42 +123,139 @@ private function saveTranslations($input, int $translatable_id, string $type) { } } } + + /** + * @param $input + * @param $translatable_id + * @param $type + * @return bool + */ + private function updateTranslations($input, int $translatable_id, string $type) { + if (!is_array($input)) { + return true; + } + Translation::where('translatable_id', $translatable_id) + ->where('translatable_type', $type) + ->delete(); + foreach ($input as $language => $translations) { + foreach ($translations as $key => $translated) { + if (is_array($translated)){ + $translated = json_encode($translated); + } + Translation::create([ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language + ]); + } + } + } /** * Display the specified resource. * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param int $id * @param Request $request - * @return \Illuminate\Http\JsonResponse + * @return mixed * @throws \Illuminate\Auth\Access\AuthorizationException */ public function update(int $id, Request $request) { $survey = Survey::find($id); $this->authorize('update', $survey); - $this->getValidationFactory()->make($request->input(), Survey::getRules()); - $survey = Survey::create( + if (!$survey) { + return response()->json( + [ + 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + ], + 404); + } + $this->validate($request, Survey::getRules()); + $survey->update( array_merge( - $request->input(),[ 'updated' => time(), 'created' => time()] + $request->input(),[ 'updated' => time()] ) ); - if ($request->input('tasks')) { - foreach ($request->input('tasks') as $stage) { - $stage_model = $survey->tasks()->create( - array_merge( - $stage, [ 'updated' => time(), 'created' => time()] - ) - ); - foreach ($stage['fields'] as $attribute) { - $uuid = Uuid::uuid4(); - $attribute['key'] = $uuid->toString(); - $stage_model->fields()->create( - array_merge( - $attribute, [ 'updated' => time(), 'created' => time()] - ) - ); + $this->updateTranslations($request->input('translations'), $survey->id, 'survey'); + $this->updateTasks($request->input('tasks') ?? [], $survey); + return new SurveyResource($survey); + } + + /** + * @param array $input_tasks + * @param Survey $survey + */ + private function updateTasks(array $input_tasks, Survey $survey) { + $added_tasks = []; + foreach ($input_tasks as $stage) { + if (isset($stage['id'])) { + $stage_model = $survey->tasks->find($stage['id']); + if (!$stage_model){ + continue; } + $stage_model->update($stage); + $stage_model = Stage::find($stage['id']); + } else { + $stage_model = $survey->tasks()->create(array_merge( + $stage, [ 'updated' => time()] + )); + $added_tasks[] = $stage_model->id; } + $this->updateTranslations($stage['translations'] ?? [], $stage_model->id, 'task'); + $this->updateFields($stage['fields'] ?? [], $stage_model); } - return response()->json(['result' => $survey->load('tasks')]); + $input_tasks_collection = new Collection($input_tasks); + $survey->load('tasks'); + + $tasks_to_delete = $survey->tasks->whereNotIn( + 'id',array_merge($added_tasks, $input_tasks_collection->groupBy('id')->keys()->toArray()) + ); + foreach ($tasks_to_delete as $task_to_delete) { + Stage::where('id',$task_to_delete->id)->delete(); + } + } + + /** + * @param array $input_fields + * @param Survey $survey + * @param Stage $stage + */ + private function updateFields(array $input_fields, Stage $stage) { + $added_fields = []; + foreach ($input_fields as $field) { + if (isset($field['id'])) { + $field_model = $stage->fields->find($field['id']); + if (!$field_model){ + continue; + } + $field_model->update($field); + $field_model = Attribute::find($field['id']); + } else { + $uuid = Uuid::uuid4(); + $field_model = $stage->fields()->create(array_merge( + $field, [ 'updated' => time(), 'key' => $uuid->toString()] + )); + $added_fields[] = $field_model->id; + } + $this->updateTranslations($field['translations'] ?? [], $field_model->id, 'field'); + } + $input_fields_collection = new Collection($input_fields); + $stage->load('fields'); + + $fields_to_delete = $stage->fields->whereNotIn( + 'id',array_merge($added_fields, $input_fields_collection->groupBy('id')->keys()->toArray()) + ); + foreach ($fields_to_delete as $field_to_delete) { + Attribute::where('id',$field_to_delete->id)->delete(); + } + } + /** + * @param int $id + */ + public function delete(int $id, Request $request) { + $survey = Survey::find($id); + $this->authorize('delete', $survey); + return response()->json(['result' => ['deleted' => $id]]); } } diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 67d55a2668..620d23822e 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -67,6 +67,7 @@ class Survey extends Model 'everyone_can_create' => true, 'hide_author' => false, 'hide_time' => false, + 'disabled' => false, 'hide_location' => false, 'targeted_survey' => false ]; @@ -130,6 +131,9 @@ protected static function getRules() { 'disabled' => [ 'boolean' ], + 'everyone_can_create' => [ + 'boolean' + ], 'hide_author' => [ 'boolean' ], @@ -139,9 +143,8 @@ protected static function getRules() { 'hide_time' => [ 'boolean' ], - // @FIXME: disabled targeted survey creation for v4 forms, need to check 'targeted_survey' => [ - Rule::in([false]), + 'boolean' ], 'tasks.*.label' => [ 'required', @@ -208,7 +211,7 @@ protected static function getRules() { //[[$this, 'checkForDuplicates'], [':validation', ':value']], ], 'tasks.*.fields.*.type' => [ - 'boolean' + 'string' ], 'tasks.*.fields.*.priority' => [ 'numeric', @@ -235,4 +238,15 @@ public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); } + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'everyone_can_create' => 'boolean', + 'hide_author' => 'boolean', + 'require_approval' => 'boolean', + 'disabled' => 'boolean' + ]; } diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index 295714fd41..cd1c5aee0c 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -1,7 +1,6 @@ isAllowed($form, 'read'); } + /** + * + * @param GenericUser $user + * @param Survey $survey + * @return bool + */ + public function delete(User $user, Survey $survey) + { + $form = new Entity\Form($survey->toArray()); + return $this->isAllowed($form, 'delete'); + } /** * @param Survey $survey * @return bool */ - public function update(Survey $survey) { + public function update(User $user, Survey $survey) { // we convert to a form entity to be able to continue using the old authorizers and classes. $form = new Entity\Form($survey->toArray()); return $this->isAllowed($form, 'update'); diff --git a/v4/routes/web.php b/v4/routes/web.php index d27b391748..c99b977032 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -25,6 +25,8 @@ 'middleware' => ['auth:api', 'scope:forms'] ], function () use ($router) { $router->post('/', 'SurveyController@store'); + $router->put('/{id}', 'SurveyController@update'); + $router->delete('/{id}', 'SurveyController@delete'); }); // Restricted access From 47b9de361cf92430ba3565dbdf57e7b257aeae05 Mon Sep 17 00:00:00 2001 From: Romina Date: Sat, 9 May 2020 23:50:38 -0300 Subject: [PATCH 23/39] Add validation errors, with translatable field names --- resources/lang/en/fields.php | 22 ++++++++++++ resources/lang/es/fields.php | 22 ++++++++++++ v4/Http/Controllers/SurveyController.php | 4 +-- v4/Models/Survey.php | 43 +++++++++++++++++++++++- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 resources/lang/en/fields.php create mode 100644 resources/lang/es/fields.php diff --git a/resources/lang/en/fields.php b/resources/lang/en/fields.php new file mode 100644 index 0000000000..14b23d7b32 --- /dev/null +++ b/resources/lang/en/fields.php @@ -0,0 +1,22 @@ + 'Name', + 'disabled' => 'Disabled', + 'everyone_can_create' => 'Who can add to this survey?', + 'hide_author' => 'Hide author', + 'hide_location' => 'Hide location', + 'hide_time' => 'Hide time', + 'targeted_survey' => 'Targeted survey', + 'tasks.label' => 'Task label', + 'tasks.type' => 'Task type', + 'tasks.priority' => 'Priority', + 'tasks.icon' => 'Icon', + 'tasks.fields.label' => 'Field label', + 'tasks.fields.key' => 'Field key', + 'tasks.fields.input' => 'Field input type', + 'tasks.fields.type' => 'Field type', + 'tasks.fields.priority' => 'Field priority', + 'tasks.fields.cardinality' => 'Field cardinality', + 'tasks.fields.response_private' => 'Should responses be private?', +); diff --git a/resources/lang/es/fields.php b/resources/lang/es/fields.php new file mode 100644 index 0000000000..5dbe259950 --- /dev/null +++ b/resources/lang/es/fields.php @@ -0,0 +1,22 @@ + 'Nombre', + 'disabled' => 'Deshabilitado', + 'everyone_can_create' => 'Quién puede agregar a esta encuesta?', + 'hide_author' => 'Ocultar autor', + 'hide_location' => 'Ocultar ubicación', + 'hide_time' => 'Ocultar horario', + 'targeted_survey' => 'Encuesta guiada', + 'tasks.label' => 'Etiqueta de tarea', + 'tasks.type' => 'Tipo de tarea', + 'tasks.priority' => 'Prioridad de tarea', + 'tasks.icon' => 'Icono de tarea', + 'tasks.fields.label' => 'Etiqueta de campo', + 'tasks.fields.key' => 'Clave de campo', + 'tasks.fields.input' => 'Tipo de campo de entrada', + 'tasks.fields.type' => 'Tipo de campo', + 'tasks.fields.priority' => 'Prioridad de campo', + 'tasks.fields.cardinality' => 'Cardinalidad de campo', + 'tasks.fields.response_private' => 'Deberian las respuestas ser privadas?', +); diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 2eb2583ff8..70921ddc8b 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -68,7 +68,7 @@ public function store(Request $request) { if ($user) { $this->authorize('store', Survey::class); } - $this->validate($request, Survey::getRules()); + $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey = Survey::create( array_merge( $request->input(),[ 'updated' => time(), 'created' => time()] @@ -171,7 +171,7 @@ public function update(int $id, Request $request) { ], 404); } - $this->validate($request, Survey::getRules()); + $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey->update( array_merge( $request->input(),[ 'updated' => time()] diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 620d23822e..bd7acb58ac 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -230,7 +230,48 @@ protected static function getRules() { // for it to be needed ]; } - + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public static function validationMessages() + { + return [ + 'name.required' => trans('validation.not_empty', ['field' => trans('fields.name')]), + 'name.min' => trans('validation.min_length', ['param2' => 2]), + 'name.max' => trans('validation.max_length', ['param2' => 255]), + 'name.regex' => trans('validation.regex', ['field' => trans('fields.name')]), + //description.string + //color.string + 'disabled.boolean' => trans('validation.regex', ['field' => trans('fields.disabled')]), + 'everyone_can_create.boolean' => trans('validation.regex', ['field' => trans('fields.everyone_can_create')]), + 'hide_author.boolean' => trans('validation.regex', ['field' => trans('fields.hide_author')]), + 'hide_location.boolean' => trans('validation.regex', ['field' => trans('fields.hide_location')]), + 'hide_time.boolean' => trans('validation.regex', ['field' => trans('fields.hide_time')]), + 'targeted_survey.boolean' => trans('validation.regex', ['field' => trans('fields.targeted_survey')]), + 'tasks.*.label.required' => trans('validation.not_empty', ['field' => trans('fields.tasks.label')]), + 'tasks.*.label.boolean' => trans('validation.regex', ['field' => trans('fields.tasks.label')]), + 'tasks.*.type.in' => trans('validation.in_array', ['field' => trans('fields.tasks.type')]), + 'tasks.*.priority.numeric' => trans('validation.numeric', ['field' => trans('fields.tasks.priority')]), + 'tasks.*.icon.alpha' => trans('validation.alpha', ['field' => trans('fields.tasks.icon')]), + 'tasks.*.fields.*.label.required' => trans('validation.not_empty', ['field' => trans('fields.tasks.fields.label')]), + 'tasks.*.fields.*.label.max' => trans('validation.max_length', ['param2' => trans('fields.tasks.fields.label')]), + 'tasks.*.fields.*.key.alpha_dash' => trans('validation.alpha_dash', ['field' => trans('fields.tasks.fields.key')]), + 'tasks.*.fields.*.key.max' => trans('validation.max_length', ['param2' => trans('fields.tasks.fields.key')]), + 'tasks.*.fields.*.input.required' => trans('validation.not_empty', ['param2' => trans('fields.tasks.fields.input')]), + 'tasks.*.fields.*.input.in' => trans('validation.in_array', ['param2' => trans('fields.tasks.fields.input')]), + 'tasks.*.fields.*.type.required' => trans('validation.not_empty', ['param2' => trans('fields.tasks.fields.type')]), + 'tasks.*.fields.*.type.in' => trans('validation.in_array', ['param2' => trans('fields.tasks.fields.type')]), + 'tasks.*.fields.*.priority.numeric' => trans('validation.numeric', ['param2' => trans('fields.tasks.fields.priority')]), + 'tasks.*.fields.*.cardinality.numeric' => trans('validation.numeric', ['param2' => trans('fields.tasks.fields.cardinality')]), + 'tasks.*.fields.*.response_private.boolean' => trans('validation.regex', ['field' => trans('fields.tasks.fields.response_private')]), + //'tasks.*.fields.*.response_private' => [ + //// @TODO add this custom validator for canMakePrivate + // [[$this, 'canMakePrivate'], [':value', $type]] + //] + ]; + } /** * Get the survey's translation. */ From 14e8cf9572554072b7040fd910f2605fa9e11ec0 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 11 May 2020 02:47:42 -0300 Subject: [PATCH 24/39] API fix for translations that use arrays --- tests/integration/v4/forms/forms.v4.feature | 42 ++++++++++----------- v4/Http/Controllers/SurveyController.php | 3 -- v4/Http/Resources/TranslationCollection.php | 9 ++++- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index a8676060b2..cf519e92b8 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -26,7 +26,7 @@ Feature: Testing the Surveys API And the response has a "result.can_create" property And the "result.can_create" property is empty Then the guzzle status code should be 201 -# + Scenario: Updating a Survey Given that I want to update a "Survey" And that the api_url is "api/v4" @@ -533,7 +533,7 @@ Feature: Testing the Surveys API And the "result.tasks.0.fields.0.label" property equals "Test varchar" And the "result.tasks.0.fields.0.translations.es.label" property equals "ES Test varchar" Then the guzzle status code should be 200 -# + Scenario: Updating a Survey to clear name should fail Given that I want to update a "Survey" And that the api_url is "api/v4" @@ -552,25 +552,25 @@ Feature: Testing the Surveys API When I request "/surveys" Then the response is JSON Then the guzzle status code should be 422 -# -# Scenario: Update a non-existent Survey -# Given that I want to update a "Survey" -# And that the api_url is "api/v4" -# And that the request "data" is: -# """ -# { -# "name":"Updated Test Survey", -# "type":"report", -# "description":"This is a test form updated by BDD testing", -# "disabled":false -# } -# """ -# And that its "id" is "40" -# When I request "/surveys" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 404 -# + + Scenario: Update a non-existent Survey + Given that I want to update a "Survey" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "name":"Updated Test Survey", + "type":"report", + "description":"This is a test form updated by BDD testing", + "disabled":false + } + """ + And that its "id" is "440" + When I request "/surveys" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 404 + Scenario: Listing All Surveys Given that I want to get all "Surveys" And that the api_url is "api/v4" diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 70921ddc8b..f71161f4e9 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -40,7 +40,6 @@ public function show(int $id) /** * Display the specified resource. - * @TODO add enabled_languages (the ones that we have translations for) * @return SurveyCollection * @throws \Illuminate\Auth\Access\AuthorizationException */ @@ -51,7 +50,6 @@ public function index() /** * Display the specified resource. - * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param Request $request * @return SurveyResource @@ -154,7 +152,6 @@ private function updateTranslations($input, int $translatable_id, string $type) } /** * Display the specified resource. - * @TODO add enabled_languages (the ones that we have translations for) * @TODO transactions =) * @param int $id * @param Request $request diff --git a/v4/Http/Resources/TranslationCollection.php b/v4/Http/Resources/TranslationCollection.php index 1332b5901e..3ba1b039d6 100644 --- a/v4/Http/Resources/TranslationCollection.php +++ b/v4/Http/Resources/TranslationCollection.php @@ -22,8 +22,15 @@ class TranslationCollection extends ResourceCollection */ public function toArray($request) { - + // translate options + // use key "options" for type "field" to do json_decode $grouped = $this->collection->mapToGroups(function ($item, $key) { + if ( + $item->translated_key === 'options' && + $item->translatable_type==='field' + ) { + $item->translation = json_decode($item->translation); + } return [$item->language => $item]; }); $combined = $grouped->map(function ($item, $key) { From b72ac657301abd074afc7da655254f35e475c7fe Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 11 May 2020 20:14:44 -0300 Subject: [PATCH 25/39] Add Json 404 for survey updates --- v4/Http/Controllers/SurveyController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index f71161f4e9..87709f585b 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -160,6 +160,13 @@ private function updateTranslations($input, int $translatable_id, string $type) */ public function update(int $id, Request $request) { $survey = Survey::find($id); + if (!$survey) { + return response()->json( + [ + 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + ], + 404); + } $this->authorize('update', $survey); if (!$survey) { return response()->json( From 720e5212ffea5ea07c575d2deadd3da5d61ed288 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 11 May 2020 21:17:01 -0300 Subject: [PATCH 26/39] Fix lint issues --- ...20200506131856_add_entity_translations.php | 3 +- .../20200509204243_add_language_to_survey.php | 3 +- src/App/DataSource/Twitter/Twitter.php | 10 +- .../App/DataSource/TwitterDataSourceTest.php | 2 +- v4/Http/Controllers/LanguagesController.php | 1646 ++++++++++++++++- v4/Http/Controllers/SurveyController.php | 68 +- v4/Http/Resources/FieldCollection.php | 1 - v4/Http/Resources/FieldResource.php | 1 + v4/Http/Resources/SurveyCollection.php | 1 - v4/Http/Resources/SurveyResource.php | 1 + v4/Http/Resources/TaskCollection.php | 1 - v4/Http/Resources/TaskResource.php | 1 + v4/Http/Resources/TranslationCollection.php | 6 +- v4/Http/Resources/TranslationResource.php | 1 + v4/Models/Attribute.php | 5 +- v4/Models/Stage.php | 6 +- v4/Models/Survey.php | 489 +++-- v4/Models/Translation.php | 2 +- v4/Policies/SurveyPolicy.php | 17 +- v4/Providers/MorphServiceProvider.php | 3 +- 20 files changed, 2023 insertions(+), 244 deletions(-) diff --git a/migrations/20200506131856_add_entity_translations.php b/migrations/20200506131856_add_entity_translations.php index e16703792d..18f3b02e48 100644 --- a/migrations/20200506131856_add_entity_translations.php +++ b/migrations/20200506131856_add_entity_translations.php @@ -16,7 +16,8 @@ public function up() ->addTimestamps() ->create(); } - public function down() { + public function down() + { $this->dropTable('translations'); } } diff --git a/migrations/20200509204243_add_language_to_survey.php b/migrations/20200509204243_add_language_to_survey.php index 454b1a3d9a..bb6d344809 100644 --- a/migrations/20200509204243_add_language_to_survey.php +++ b/migrations/20200509204243_add_language_to_survey.php @@ -17,7 +17,8 @@ public function up() ->update(); } - public function down() { + public function down() + { $this->table('forms')->removeColumn('base_language'); } } diff --git a/src/App/DataSource/Twitter/Twitter.php b/src/App/DataSource/Twitter/Twitter.php index 44b48162b5..f9b8545483 100644 --- a/src/App/DataSource/Twitter/Twitter.php +++ b/src/App/DataSource/Twitter/Twitter.php @@ -189,21 +189,23 @@ public function fetch($limit = false) } $user_id = $user['id_str']; // @todo Check for similar messages in the database before saving - /*** + /*** * Twitter links note: (message field) - * Best compromise I could find was just make the proper urls with user_id rather than only + * Best compromise I could find was just make the + * proper urls with user_id rather than only * tweet id (for which there is an unofficial formula)... * since there doesn't seem to be a way to grab the URL from the * API itself in the v1.1 search endpoint. * Fun fact: if the user id is wrong, twitter - * still takes you to the correct Tweet... they just use the tweet id + * still takes you to the correct Tweet... + * they just use the tweet id **/ $messages[] = [ 'type' => MessageType::TWITTER, 'contact_type' => Contact::TWITTER, 'from' => $user_id, 'to' => null, - 'message' => "https://twitter.com/$user_id/status/$id", + 'message' => "https://twitter.com/$user_id/status/$id", 'title' => 'From twitter on ' . $date, 'datetime' => $date, 'data_source_message_id' => $id, diff --git a/tests/unit/App/DataSource/TwitterDataSourceTest.php b/tests/unit/App/DataSource/TwitterDataSourceTest.php index 780f3abf3a..6c7d73121d 100644 --- a/tests/unit/App/DataSource/TwitterDataSourceTest.php +++ b/tests/unit/App/DataSource/TwitterDataSourceTest.php @@ -282,7 +282,7 @@ function ($a, $b, $c, $d) use ($mockTwitterOAuth) { 'type' => 'twitter', 'contact_type' => 'twitter', 'from' => '1112222225', // twitter user id - 'message' => 'https://twitter.com/1112222225/status/abc125', + 'message' => 'https://twitter.com/1112222225/status/abc125', 'to' => null, 'title' => 'From twitter on Thu Apr 06 15:24:15 +0000 2017', 'data_source_message_id' => 'abc125', diff --git a/v4/Http/Controllers/LanguagesController.php b/v4/Http/Controllers/LanguagesController.php index f4510ca5d1..cfddfee82f 100644 --- a/v4/Http/Controllers/LanguagesController.php +++ b/v4/Http/Controllers/LanguagesController.php @@ -1,19 +1,1657 @@ + * @package Ushahidi\Application + * @license https://www.gnu.org/licenses/agpl-3.0.html (AGPL3) + * @copyright 2020 Ushahidi + */ + namespace v4\Http\Controllers; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; -use Laravel\Lumen\Routing\Controller as BaseController; class LanguagesController extends V4Controller { + + + /** + * An silly endpoint that returns an array of languages + * + * @return \Illuminate\Http\JsonResponse + */ public function index() { $languages = [ - ["code" => "ach", "name" => "Acoli"], ["code" => "ady", "name" => "Adyghe"], ["code" => "af", "name" => "Afrikaans"], ["code" => "af-ZA", "name" => "Afrikaans (South Africa)"], ["code" => "ak", "name" => "Akan"], ["code" => "sq", "name" => "Albanian"], ["code" => "sq-AL", "name" => "Albanian (Albania)"], ["code" => "aln", "name" => "Albanian Gheg"], ["code" => "am", "name" => "Amharic"], ["code" => "am-ET", "name" => "Amharic (Ethiopia)"], ["code" => "ar", "name" => "Arabic"], ["code" => "ar-EG", "name" => "Arabic (Egypt)"], ["code" => "ar-SA", "name" => "Arabic (Saudi Arabia)"], ["code" => "ar-SD", "name" => "Arabic (Sudan)"], ["code" => "ar-SY", "name" => "Arabic (Syria)"], ["code" => "ar-AA", "name" => "Arabic (Unitag)"], ["code" => "an", "name" => "Aragonese"], ["code" => "hy", "name" => "Armenian"], ["code" => "hy-AM", "name" => "Armenian (Armenia)"], ["code" => "as", "name" => "Assamese"], ["code" => "as-IN", "name" => "Assamese (India)"], ["code" => "ast", "name" => "Asturian"], ["code" => "ast-ES", "name" => "Asturian (Spain)"], ["code" => "az", "name" => "Azerbaijani"], ["code" => "az@Arab", "name" => "Azerbaijani (Arabic)"], ["code" => "az-AZ", "name" => "Azerbaijani (Azerbaijan)"], ["code" => "az-IR", "name" => "Azerbaijani (Iran)"], ["code" => "az@latin", "name" => "Azerbaijani (Latin)"], ["code" => "bal", "name" => "Balochi"], ["code" => "ba", "name" => "Bashkir"], ["code" => "eu", "name" => "Basque"], ["code" => "eu-ES", "name" => "Basque (Spain)"], ["code" => "bar", "name" => "Bavarian"], ["code" => "be", "name" => "Belarusian"], ["code" => "be-BY", "name" => "Belarusian (Belarus)"], ["code" => "be@tarask", "name" => "Belarusian (Tarask)"], ["code" => "bn", "name" => "Bengali"], ["code" => "bn-BD", "name" => "Bengali (Bangladesh)"], ["code" => "bn-IN", "name" => "Bengali (India)"], ["code" => "brx", "name" => "Bodo"], ["code" => "bs", "name" => "Bosnian"], ["code" => "bs-BA", "name" => "Bosnian (Bosnia and Herzegovina)"], ["code" => "br", "name" => "Breton"], ["code" => "bg", "name" => "Bulgarian"], ["code" => "bg-BG", "name" => "Bulgarian (Bulgaria)"], ["code" => "my", "name" => "Burmese"], ["code" => "my-MM", "name" => "Burmese (Myanmar)"], ["code" => "ca", "name" => "Catalan"], ["code" => "ca-ES", "name" => "Catalan (Spain)"], ["code" => "ca@valencia", "name" => "Catalan (Valencian)"], ["code" => "ceb", "name" => "Cebuano"], ["code" => "tzm", "name" => "Central Atlas Tamazight"], ["code" => "hne", "name" => "Chhattisgarhi"], ["code" => "cgg", "name" => "Chiga"], ["code" => "zh", "name" => "Chinese"], ["code" => "zh-CN", "name" => "Chinese (China)"], ["code" => "zh-CN.GB2312", "name" => "Chinese (China) (GB2312)"], ["code" => "gan", "name" => "Chinese (Gan)"], ["code" => "hak", "name" => "Chinese (Hakka)"], ["code" => "zh-HK", "name" => "Chinese (Hong Kong)"], ["code" => "czh", "name" => "Chinese (Huizhou)"], ["code" => "cjy", "name" => "Chinese (Jinyu)"], ["code" => "lzh", "name" => "Chinese (Literary)"], ["code" => "cmn", "name" => "Chinese (Mandarin)"], ["code" => "mnp", "name" => "Chinese (Min Bei)"], ["code" => "cdo", "name" => "Chinese (Min Dong)"], ["code" => "nan", "name" => "Chinese (Min Nan)"], ["code" => "czo", "name" => "Chinese (Min Zhong)"], ["code" => "cpx", "name" => "Chinese (Pu-Xian)"], ["code" => "zh-Hans", "name" => "Chinese Simplified"], ["code" => "zh-TW", "name" => "Chinese (Taiwan)"], ["code" => "zh-TW.Big5", "name" => "Chinese (Taiwan) (Big5) "], ["code" => "zh-Hant", "name" => "Chinese Traditional"], ["code" => "wuu", "name" => "Chinese (Wu)"], ["code" => "hsn", "name" => "Chinese (Xiang)"], ["code" => "yue", "name" => "Chinese (Yue)"], ["code" => "cv", "name" => "Chuvash"], ["code" => "ksh", "name" => "Colognian"], ["code" => "kw", "name" => "Cornish"], ["code" => "co", "name" => "Corsican"], ["code" => "crh", "name" => "Crimean Turkish"], ["code" => "hr", "name" => "Croatian"], ["code" => "hr-HR", "name" => "Croatian (Croatia)"], ["code" => "cs", "name" => "Czech"], ["code" => "cs-CZ", "name" => "Czech (Czech Republic)"], ["code" => "da", "name" => "Danish"], ["code" => "da-DK", "name" => "Danish (Denmark)"], ["code" => "dv", "name" => "Divehi"], ["code" => "doi", "name" => "Dogri"], ["code" => "nl", "name" => "Dutch"], ["code" => "nl-BE", "name" => "Dutch (Belgium)"], ["code" => "nl-NL", "name" => "Dutch (Netherlands)"], ["code" => "dz", "name" => "Dzongkha"], ["code" => "dz-BT", "name" => "Dzongkha (Bhutan)"], ["code" => "en", "name" => "English"], ["code" => "en-AU", "name" => "English (Australia)"], ["code" => "en-AT", "name" => "English (Austria)"], ["code" => "en-BD", "name" => "English (Bangladesh)"], ["code" => "en-BE", "name" => "English (Belgium)"], ["code" => "en-CA", "name" => "English (Canada)"], ["code" => "en-CL", "name" => "English (Chile)"], ["code" => "en-CZ", "name" => "English (Czech Republic)"], ["code" => "en-ee", "name" => "English (Estonia)"], ["code" => "en-FI", "name" => "English (Finland)"], ["code" => "en-DE", "name" => "English (Germany)"], ["code" => "en-GH", "name" => "English (Ghana)"], ["code" => "en-HK", "name" => "English (Hong Kong)"], ["code" => "en-HU", "name" => "English (Hungary)"], ["code" => "en-IN", "name" => "English (India)"], ["code" => "en-IE", "name" => "English (Ireland)"], ["code" => "en-lv", "name" => "English (Latvia)"], ["code" => "en-lt", "name" => "English (Lithuania)"], ["code" => "en-NL", "name" => "English (Netherlands)"], ["code" => "en-NZ", "name" => "English (New Zealand)"], ["code" => "en-NG", "name" => "English (Nigeria)"], ["code" => "en-PK", "name" => "English (Pakistan)"], ["code" => "en-PL", "name" => "English (Poland)"], ["code" => "en-RO", "name" => "English (Romania)"], ["code" => "en-SK", "name" => "English (Slovakia)"], ["code" => "en-ZA", "name" => "English (South Africa)"], ["code" => "en-LK", "name" => "English (Sri Lanka)"], ["code" => "en-SE", "name" => "English (Sweden)"], ["code" => "en-CH", "name" => "English (Switzerland)"], ["code" => "en-GB", "name" => "English (United Kingdom)"], ["code" => "en-US", "name" => "English (United States)"], ["code" => "en-EN", "name" => "English"], ["code" => "myv", "name" => "Erzya"], ["code" => "eo", "name" => "Esperanto"], ["code" => "et", "name" => "Estonian"], ["code" => "et-EE", "name" => "Estonian (Estonia)"], ["code" => "fo", "name" => "Faroese"], ["code" => "fo-FO", "name" => "Faroese (Faroe Islands)"], ["code" => "fil", "name" => "Filipino"], ["code" => "fi", "name" => "Finnish"], ["code" => "fi-FI", "name" => "Finnish (Finland)"], ["code" => "frp", "name" => "Franco-Provençal (Arpitan)"], ["code" => "fr", "name" => "French"], ["code" => "fr-BE", "name" => "French (Belgium)"], ["code" => "fr-CA", "name" => "French (Canada)"], ["code" => "fr-FR", "name" => "French (France)"], ["code" => "fr-CH", "name" => "French (Switzerland)"], ["code" => "fur", "name" => "Friulian"], ["code" => "ff", "name" => "Fulah"], ["code" => "ff-SN", "name" => "Fulah (Senegal)"], ["code" => "gd", "name" => "Gaelic, Scottish"], ["code" => "gl", "name" => "Galician"], ["code" => "gl-ES", "name" => "Galician (Spain)"], ["code" => "lg", "name" => "Ganda"], ["code" => "ka", "name" => "Georgian"], ["code" => "ka-GE", "name" => "Georgian (Georgia)"], ["code" => "de", "name" => "German"], ["code" => "de-AT", "name" => "German (Austria)"], ["code" => "de-DE", "name" => "German (Germany)"], ["code" => "de-CH", "name" => "German (Switzerland)"], ["code" => "el", "name" => "Greek"], ["code" => "el-GR", "name" => "Greek (Greece)"], ["code" => "kl", "name" => "Greenlandic"], ["code" => "gu", "name" => "Gujarati"], ["code" => "gu-IN", "name" => "Gujarati (India)"], ["code" => "gun", "name" => "Gun"], ["code" => "ht", "name" => "Haitian (Haitian Creole)"], ["code" => "ht-HT", "name" => "Haitian (Haitian Creole) (Haiti)"], ["code" => "ha", "name" => "Hausa"], ["code" => "haw", "name" => "Hawaiian"], ["code" => "he", "name" => "Hebrew"], ["code" => "he-IL", "name" => "Hebrew (Israel)"], ["code" => "hi", "name" => "Hindi"], ["code" => "hi-IN", "name" => "Hindi (India)"], ["code" => "hu", "name" => "Hungarian"], ["code" => "hu-HU", "name" => "Hungarian (Hungary)"], ["code" => "is", "name" => "Icelandic"], ["code" => "is-IS", "name" => "Icelandic (Iceland)"], ["code" => "io", "name" => "Ido"], ["code" => "ig", "name" => "Igbo"], ["code" => "ilo", "name" => "Iloko"], ["code" => "id", "name" => "Indonesian"], ["code" => "id-ID", "name" => "Indonesian (Indonesia)"], ["code" => "ia", "name" => "Interlingua"], ["code" => "iu", "name" => "Inuktitut"], ["code" => "ga", "name" => "Irish"], ["code" => "ga-IE", "name" => "Irish (Ireland)"], ["code" => "it", "name" => "Italian"], ["code" => "it-IT", "name" => "Italian (Italy)"], ["code" => "it-CH", "name" => "Italian (Switzerland)"], ["code" => "ja", "name" => "Japanese"], ["code" => "ja-JP", "name" => "Japanese (Japan)"], ["code" => "jv", "name" => "Javanese"], ["code" => "kab", "name" => "Kabyle"], ["code" => "kn", "name" => "Kannada"], ["code" => "kn-IN", "name" => "Kannada (India)"], ["code" => "pam", "name" => "Kapampangan"], ["code" => "ks", "name" => "Kashmiri"], ["code" => "ks-IN", "name" => "Kashmiri (India)"], ["code" => "csb", "name" => "Kashubian"], ["code" => "kk", "name" => "Kazakh"], ["code" => "kk@Arab", "name" => "Kazakh (Arabic)"], ["code" => "kk@Cyrl", "name" => "Kazakh (Cyrillic)"], ["code" => "kk-KZ", "name" => "Kazakh (Kazakhstan)"], ["code" => "kk@latin", "name" => "Kazakh (Latin)"], ["code" => "km", "name" => "Khmer"], ["code" => "km-KH", "name" => "Khmer (Cambodia)"], ["code" => "rw", "name" => "Kinyarwanda"], ["code" => "ky", "name" => "Kirgyz"], ["code" => "tlh", "name" => "Klingon"], ["code" => "kok", "name" => "Konkani"], ["code" => "ko", "name" => "Korean"], ["code" => "ko-KR", "name" => "Korean (Korea)"], ["code" => "ku", "name" => "Kurdish"], ["code" => "ku-IQ", "name" => "Kurdish (Iraq)"], ["code" => "lad", "name" => "Ladino"], ["code" => "lo", "name" => "Lao"], ["code" => "lo-LA", "name" => "Lao (Laos)"], ["code" => "ltg", "name" => "Latgalian"], ["code" => "la", "name" => "Latin"], ["code" => "lv", "name" => "Latvian"], ["code" => "lv-LV", "name" => "Latvian (Latvia)"], ["code" => "lez", "name" => "Lezghian"], ["code" => "lij", "name" => "Ligurian"], ["code" => "li", "name" => "Limburgian"], ["code" => "ln", "name" => "Lingala"], ["code" => "lt", "name" => "Lithuanian"], ["code" => "lt-LT", "name" => "Lithuanian (Lithuania)"], ["code" => "jbo", "name" => "Lojban"], ["code" => "en@lolcat", "name" => "LOLCAT English"], ["code" => "lmo", "name" => "Lombard"], ["code" => "dsb", "name" => "Lower Sorbian"], ["code" => "nds", "name" => "Low German"], ["code" => "lb", "name" => "Luxembourgish"], ["code" => "mk", "name" => "Macedonian"], ["code" => "mk-MK", "name" => "Macedonian (Macedonia)"], ["code" => "mai", "name" => "Maithili"], ["code" => "mg", "name" => "Malagasy"], ["code" => "ms", "name" => "Malay"], ["code" => "ml", "name" => "Malayalam"], ["code" => "ml-IN", "name" => "Malayalam (India)"], ["code" => "ms-MY", "name" => "Malay (Malaysia)"], ["code" => "mt", "name" => "Maltese"], ["code" => "mt-MT", "name" => "Maltese (Malta)"], ["code" => "mni", "name" => "Manipuri"], ["code" => "mi", "name" => "Maori"], ["code" => "arn", "name" => "Mapudungun"], ["code" => "mr", "name" => "Marathi"], ["code" => "mr-IN", "name" => "Marathi (India)"], ["code" => "mh", "name" => "Marshallese"], ["code" => "mw1", "name" => "Mirandese"], ["code" => "mn", "name" => "Mongolian"], ["code" => "mn-MN", "name" => "Mongolian (Mongolia)"], ["code" => "nah", "name" => "Nahuatl"], ["code" => "nv", "name" => "Navajo"], ["code" => "nr", "name" => "Ndebele, South"], ["code" => "nap", "name" => "Neapolitan"], ["code" => "ne", "name" => "Nepali"], ["code" => "ne-NP", "name" => "Nepali (Nepal)"], ["code" => "nia", "name" => "Nias"], ["code" => "nqo", "name" => "N'ko"], ["code" => "se", "name" => "Northern Sami"], ["code" => "nso", "name" => "Northern Sotho"], ["code" => "no", "name" => "Norwegian"], ["code" => "nb", "name" => "Norwegian Bokmål"], ["code" => "nb-NO", "name" => "Norwegian Bokmål (Norway)"], ["code" => "no-NO", "name" => "Norwegian (Norway)"], ["code" => "nn", "name" => "Norwegian Nynorsk"], ["code" => "nn-NO", "name" => "Norwegian Nynorsk (Norway)"], ["code" => "ny", "name" => "Nyanja"], ["code" => "oc", "name" => "Occitan (post 1500)"], ["code" => "or", "name" => "Oriya"], ["code" => "or-IN", "name" => "Oriya (India)"], ["code" => "om", "name" => "Oromo"], ["code" => "os", "name" => "Ossetic"], ["code" => "pfl", "name" => "Palatinate German"], ["code" => "pa", "name" => "Panjabi (Punjabi)"], ["code" => "pa-IN", "name" => "Panjabi (Punjabi) (India)"], ["code" => "pap", "name" => "Papiamento"], ["code" => "fa", "name" => "Persian"], ["code" => "fa-AF", "name" => "Persian (Afghanistan)"], ["code" => "fa-IR", "name" => "Persian (Iran)"], ["code" => "pms", "name" => "Piemontese"], ["code" => "en@pirate", "name" => "Pirate English"], ["code" => "pl", "name" => "Polish"], ["code" => "pl-PL", "name" => "Polish (Poland)"], ["code" => "pt", "name" => "Portuguese"], ["code" => "pt-BR", "name" => "Portuguese (Brazil)"], ["code" => "pt-PT", "name" => "Portuguese (Portugal)"], ["code" => "ps", "name" => "Pushto"], ["code" => "ro", "name" => "Romanian"], ["code" => "ro-RO", "name" => "Romanian (Romania)"], ["code" => "rm", "name" => "Romansh"], ["code" => "ru", "name" => "Russian"], ["code" => "ru-ee", "name" => "Russian (Estonia)"], ["code" => "ru-lv", "name" => "Russian (Latvia)"], ["code" => "ru-lt", "name" => "Russian (Lithuania)"], ["code" => "ru@petr1708", "name" => "Russian Petrine orthography"], ["code" => "ru-RU", "name" => "Russian (Russia)"], ["code" => "sah", "name" => "Sakha (Yakut)"], ["code" => "sm", "name" => "Samoan"], ["code" => "sa", "name" => "Sanskrit"], ["code" => "sat", "name" => "Santali"], ["code" => "sc", "name" => "Sardinian"], ["code" => "sco", "name" => "Scots"], ["code" => "sr", "name" => "Serbian"], ["code" => "sr@Ijekavian", "name" => "Serbian (Ijekavian)"], ["code" => "sr@ijekavianlatin", "name" => "Serbian (Ijekavian Latin)"], ["code" => "sr@latin", "name" => "Serbian (Latin)"], ["code" => "sr-RS@latin", "name" => "Serbian (Latin) (Serbia)"], ["code" => "sr-RS", "name" => "Serbian (Serbia)"], ["code" => "sn", "name" => "Shona"], ["code" => "scn", "name" => "Sicilian"], ["code" => "szl", "name" => "Silesian"], ["code" => "sd", "name" => "Sindhi"], ["code" => "si", "name" => "Sinhala"], ["code" => "si-LK", "name" => "Sinhala (Sri Lanka)"], ["code" => "sk", "name" => "Slovak"], ["code" => "sk-SK", "name" => "Slovak (Slovakia)"], ["code" => "sl", "name" => "Slovenian"], ["code" => "sl-SI", "name" => "Slovenian (Slovenia)"], ["code" => "so", "name" => "Somali"], ["code" => "son", "name" => "Songhay"], ["code" => "st", "name" => "Sotho, Southern"], ["code" => "st-ZA", "name" => "Sotho, Southern (South Africa)"], ["code" => "sma", "name" => "Southern Sami"], ["code" => "es", "name" => "Spanish"], ["code" => "es-AR", "name" => "Spanish (Argentina)"], ["code" => "es-BO", "name" => "Spanish (Bolivia)"], ["code" => "es-CL", "name" => "Spanish (Chile)"], ["code" => "es-CO", "name" => "Spanish (Colombia)"], ["code" => "es-CR", "name" => "Spanish (Costa Rica)"], ["code" => "es-DO", "name" => "Spanish (Dominican Republic)"], ["code" => "es-EC", "name" => "Spanish (Ecuador)"], ["code" => "es-SV", "name" => "Spanish (El Salvador)"], ["code" => "es-GT", "name" => "Spanish (Guatemala)"], ["code" => "es-419", "name" => "Spanish (Latin America)"], ["code" => "es-MX", "name" => "Spanish (Mexico)"], ["code" => "es-NI", "name" => "Spanish (Nicaragua)"], ["code" => "es-PA", "name" => "Spanish (Panama)"], ["code" => "es-PY", "name" => "Spanish (Paraguay)"], ["code" => "es-PE", "name" => "Spanish (Peru)"], ["code" => "es-PR", "name" => "Spanish (Puerto Rico)"], ["code" => "es-ES", "name" => "Spanish (Spain)"], ["code" => "es-US", "name" => "Spanish (United States)"], ["code" => "es-UY", "name" => "Spanish (Uruguay)"], ["code" => "es-VE", "name" => "Spanish (Venezuela)"], ["code" => "su", "name" => "Sundanese"], ["code" => "sw", "name" => "Swahili"], ["code" => "sw-KE", "name" => "Swahili (Kenya)"], ["code" => "ss", "name" => "Swati"], ["code" => "sv", "name" => "Swedish"], ["code" => "sv-FI", "name" => "Swedish (Finland)"], ["code" => "sv-SE", "name" => "Swedish (Sweden)"], ["code" => "tl", "name" => "Tagalog"], ["code" => "tl-PH", "name" => "Tagalog (Philippines)"], ["code" => "tg", "name" => "Tajik"], ["code" => "tg-TJ", "name" => "Tajik (Tajikistan)"], ["code" => "tzl", "name" => "Talossan"], ["code" => "ta", "name" => "Tamil"], ["code" => "ta-IN", "name" => "Tamil (India)"], ["code" => "ta-LK", "name" => "Tamil (Sri-Lanka)"], ["code" => "tt", "name" => "Tatar"], ["code" => "te", "name" => "Telugu"], ["code" => "te-IN", "name" => "Telugu (India)"], ["code" => "tet", "name" => "Tetum (Tetun)"], ["code" => "th", "name" => "Thai"], ["code" => "th-TH", "name" => "Thai (Thailand)"], ["code" => "bo", "name" => "Tibetan"], ["code" => "bo-CN", "name" => "Tibetan (China)"], ["code" => "ti", "name" => "Tigrinya"], ["code" => "to", "name" => "Tongan"], ["code" => "ts", "name" => "Tsonga"], ["code" => "tn", "name" => "Tswana"], ["code" => "tr", "name" => "Turkish"], ["code" => "tr-TR", "name" => "Turkish (Turkey)"], ["code" => "tk", "name" => "Turkmen"], ["code" => "tk-TM", "name" => "Turkmen (Turkmenistan)"], ["code" => "udm", "name" => "Udmurt"], ["code" => "ug", "name" => "Uighur"], ["code" => "ug@Arab", "name" => "Uighur (Arabic)"], ["code" => "ug@Cyrl", "name" => "Uighur (Cyrillic)"], ["code" => "ug@Latin", "name" => "Uighur (Latin)"], ["code" => "uk", "name" => "Ukrainian"], ["code" => "uk-UA", "name" => "Ukrainian (Ukraine)"], ["code" => "vmf", "name" => "Upper Franconian"], ["code" => "hsb", "name" => "Upper Sorbian"], ["code" => "ur", "name" => "Urdu"], ["code" => "ur-PK", "name" => "Urdu (Pakistan)"], ["code" => "uz", "name" => "Uzbek"], ["code" => "uz@Arab", "name" => "Uzbek (Arabic)"], ["code" => "uz@Cyrl", "name" => "Uzbek (Cyrillic)"], ["code" => "uz@Latn", "name" => "Uzbek (Latin)"], ["code" => "uz-UZ", "name" => "Uzbek (Uzbekistan)"], ["code" => "ve", "name" => "Venda"], ["code" => "vec", "name" => "Venetian"], ["code" => "vi", "name" => "Vietnamese"], ["code" => "vi-VN", "name" => "Vietnamese (Viet Nam)"], ["code" => "vls", "name" => "Vlaams"], ["code" => "wa", "name" => "Walloon"], ["code" => "war", "name" => "Wáray-Wáray"], ["code" => "cy", "name" => "Welsh"], ["code" => "cy-GB", "name" => "Welsh (United Kingdom)"], ["code" => "fy", "name" => "Western Frisian"], ["code" => "fy-NL", "name" => "Western Frisian (Netherlands)"], ["code" => "wo", "name" => "Wolof"], ["code" => "wo-SN", "name" => "Wolof (Senegal)"], ["code" => "xh", "name" => "Xhosa"], ["code" => "yi", "name" => "Yiddish"], ["code" => "yo", "name" => "Yoruba"], ["code" => "zu", "name" => "Zulu"], ["code" => "zu-ZA", "name" => "Zulu (South Africa)"] + [ + 'code' => 'ach', + 'name' => 'Acoli', + ], + [ + 'code' => 'ady', + 'name' => 'Adyghe', + ], + [ + 'code' => 'af', + 'name' => 'Afrikaans', + ], + [ + 'code' => 'af-ZA', + 'name' => 'Afrikaans (South Africa)', + ], + [ + 'code' => 'ak', + 'name' => 'Akan', + ], + [ + 'code' => 'sq', + 'name' => 'Albanian', + ], + [ + 'code' => 'sq-AL', + 'name' => 'Albanian (Albania)', + ], + [ + 'code' => 'aln', + 'name' => 'Albanian Gheg', + ], + [ + 'code' => 'am', + 'name' => 'Amharic', + ], + [ + 'code' => 'am-ET', + 'name' => 'Amharic (Ethiopia)', + ], + [ + 'code' => 'ar', + 'name' => 'Arabic', + ], + [ + 'code' => 'ar-EG', + 'name' => 'Arabic (Egypt)', + ], + [ + 'code' => 'ar-SA', + 'name' => 'Arabic (Saudi Arabia)', + ], + [ + 'code' => 'ar-SD', + 'name' => 'Arabic (Sudan)', + ], + [ + 'code' => 'ar-SY', + 'name' => 'Arabic (Syria)', + ], + [ + 'code' => 'ar-AA', + 'name' => 'Arabic (Unitag)', + ], + [ + 'code' => 'an', + 'name' => 'Aragonese', + ], + [ + 'code' => 'hy', + 'name' => 'Armenian', + ], + [ + 'code' => 'hy-AM', + 'name' => 'Armenian (Armenia)', + ], + [ + 'code' => 'as', + 'name' => 'Assamese', + ], + [ + 'code' => 'as-IN', + 'name' => 'Assamese (India)', + ], + [ + 'code' => 'ast', + 'name' => 'Asturian', + ], + [ + 'code' => 'ast-ES', + 'name' => 'Asturian (Spain)', + ], + [ + 'code' => 'az', + 'name' => 'Azerbaijani', + ], + [ + 'code' => 'az@Arab', + 'name' => 'Azerbaijani (Arabic)', + ], + [ + 'code' => 'az-AZ', + 'name' => 'Azerbaijani (Azerbaijan)', + ], + [ + 'code' => 'az-IR', + 'name' => 'Azerbaijani (Iran)', + ], + [ + 'code' => 'az@latin', + 'name' => 'Azerbaijani (Latin)', + ], + [ + 'code' => 'bal', + 'name' => 'Balochi', + ], + [ + 'code' => 'ba', + 'name' => 'Bashkir', + ], + [ + 'code' => 'eu', + 'name' => 'Basque', + ], + [ + 'code' => 'eu-ES', + 'name' => 'Basque (Spain)', + ], + [ + 'code' => 'bar', + 'name' => 'Bavarian', + ], + [ + 'code' => 'be', + 'name' => 'Belarusian', + ], + [ + 'code' => 'be-BY', + 'name' => 'Belarusian (Belarus)', + ], + [ + 'code' => 'be@tarask', + 'name' => 'Belarusian (Tarask)', + ], + [ + 'code' => 'bn', + 'name' => 'Bengali', + ], + [ + 'code' => 'bn-BD', + 'name' => 'Bengali (Bangladesh)', + ], + [ + 'code' => 'bn-IN', + 'name' => 'Bengali (India)', + ], + [ + 'code' => 'brx', + 'name' => 'Bodo', + ], + [ + 'code' => 'bs', + 'name' => 'Bosnian', + ], + [ + 'code' => 'bs-BA', + 'name' => 'Bosnian (Bosnia and Herzegovina)', + ], + [ + 'code' => 'br', + 'name' => 'Breton', + ], + [ + 'code' => 'bg', + 'name' => 'Bulgarian', + ], + [ + 'code' => 'bg-BG', + 'name' => 'Bulgarian (Bulgaria)', + ], + [ + 'code' => 'my', + 'name' => 'Burmese', + ], + [ + 'code' => 'my-MM', + 'name' => 'Burmese (Myanmar)', + ], + [ + 'code' => 'ca', + 'name' => 'Catalan', + ], + [ + 'code' => 'ca-ES', + 'name' => 'Catalan (Spain)', + ], + [ + 'code' => 'ca@valencia', + 'name' => 'Catalan (Valencian)', + ], + [ + 'code' => 'ceb', + 'name' => 'Cebuano', + ], + [ + 'code' => 'tzm', + 'name' => 'Central Atlas Tamazight', + ], + [ + 'code' => 'hne', + 'name' => 'Chhattisgarhi', + ], + [ + 'code' => 'cgg', + 'name' => 'Chiga', + ], + [ + 'code' => 'zh', + 'name' => 'Chinese', + ], + [ + 'code' => 'zh-CN', + 'name' => 'Chinese (China)', + ], + [ + 'code' => 'zh-CN.GB2312', + 'name' => 'Chinese (China) (GB2312)', + ], + [ + 'code' => 'gan', + 'name' => 'Chinese (Gan)', + ], + [ + 'code' => 'hak', + 'name' => 'Chinese (Hakka)', + ], + [ + 'code' => 'zh-HK', + 'name' => 'Chinese (Hong Kong)', + ], + [ + 'code' => 'czh', + 'name' => 'Chinese (Huizhou)', + ], + [ + 'code' => 'cjy', + 'name' => 'Chinese (Jinyu)', + ], + [ + 'code' => 'lzh', + 'name' => 'Chinese (Literary)', + ], + [ + 'code' => 'cmn', + 'name' => 'Chinese (Mandarin)', + ], + [ + 'code' => 'mnp', + 'name' => 'Chinese (Min Bei)', + ], + [ + 'code' => 'cdo', + 'name' => 'Chinese (Min Dong)', + ], + [ + 'code' => 'nan', + 'name' => 'Chinese (Min Nan)', + ], + [ + 'code' => 'czo', + 'name' => 'Chinese (Min Zhong)', + ], + [ + 'code' => 'cpx', + 'name' => 'Chinese (Pu-Xian)', + ], + [ + 'code' => 'zh-Hans', + 'name' => 'Chinese Simplified', + ], + [ + 'code' => 'zh-TW', + 'name' => 'Chinese (Taiwan)', + ], + [ + 'code' => 'zh-TW.Big5', + 'name' => 'Chinese (Taiwan) (Big5) ', + ], + [ + 'code' => 'zh-Hant', + 'name' => 'Chinese Traditional', + ], + [ + 'code' => 'wuu', + 'name' => 'Chinese (Wu)', + ], + [ + 'code' => 'hsn', + 'name' => 'Chinese (Xiang)', + ], + [ + 'code' => 'yue', + 'name' => 'Chinese (Yue)', + ], + [ + 'code' => 'cv', + 'name' => 'Chuvash', + ], + [ + 'code' => 'ksh', + 'name' => 'Colognian', + ], + [ + 'code' => 'kw', + 'name' => 'Cornish', + ], + [ + 'code' => 'co', + 'name' => 'Corsican', + ], + [ + 'code' => 'crh', + 'name' => 'Crimean Turkish', + ], + [ + 'code' => 'hr', + 'name' => 'Croatian', + ], + [ + 'code' => 'hr-HR', + 'name' => 'Croatian (Croatia)', + ], + [ + 'code' => 'cs', + 'name' => 'Czech', + ], + [ + 'code' => 'cs-CZ', + 'name' => 'Czech (Czech Republic)', + ], + [ + 'code' => 'da', + 'name' => 'Danish', + ], + [ + 'code' => 'da-DK', + 'name' => 'Danish (Denmark)', + ], + [ + 'code' => 'dv', + 'name' => 'Divehi', + ], + [ + 'code' => 'doi', + 'name' => 'Dogri', + ], + [ + 'code' => 'nl', + 'name' => 'Dutch', + ], + [ + 'code' => 'nl-BE', + 'name' => 'Dutch (Belgium)', + ], + [ + 'code' => 'nl-NL', + 'name' => 'Dutch (Netherlands)', + ], + [ + 'code' => 'dz', + 'name' => 'Dzongkha', + ], + [ + 'code' => 'dz-BT', + 'name' => 'Dzongkha (Bhutan)', + ], + [ + 'code' => 'en', + 'name' => 'English', + ], + [ + 'code' => 'en-AU', + 'name' => 'English (Australia)', + ], + [ + 'code' => 'en-AT', + 'name' => 'English (Austria)', + ], + [ + 'code' => 'en-BD', + 'name' => 'English (Bangladesh)', + ], + [ + 'code' => 'en-BE', + 'name' => 'English (Belgium)', + ], + [ + 'code' => 'en-CA', + 'name' => 'English (Canada)', + ], + [ + 'code' => 'en-CL', + 'name' => 'English (Chile)', + ], + [ + 'code' => 'en-CZ', + 'name' => 'English (Czech Republic)', + ], + [ + 'code' => 'en-ee', + 'name' => 'English (Estonia)', + ], + [ + 'code' => 'en-FI', + 'name' => 'English (Finland)', + ], + [ + 'code' => 'en-DE', + 'name' => 'English (Germany)', + ], + [ + 'code' => 'en-GH', + 'name' => 'English (Ghana)', + ], + [ + 'code' => 'en-HK', + 'name' => 'English (Hong Kong)', + ], + [ + 'code' => 'en-HU', + 'name' => 'English (Hungary)', + ], + [ + 'code' => 'en-IN', + 'name' => 'English (India)', + ], + [ + 'code' => 'en-IE', + 'name' => 'English (Ireland)', + ], + [ + 'code' => 'en-lv', + 'name' => 'English (Latvia)', + ], + [ + 'code' => 'en-lt', + 'name' => 'English (Lithuania)', + ], + [ + 'code' => 'en-NL', + 'name' => 'English (Netherlands)', + ], + [ + 'code' => 'en-NZ', + 'name' => 'English (New Zealand)', + ], + [ + 'code' => 'en-NG', + 'name' => 'English (Nigeria)', + ], + [ + 'code' => 'en-PK', + 'name' => 'English (Pakistan)', + ], + [ + 'code' => 'en-PL', + 'name' => 'English (Poland)', + ], + [ + 'code' => 'en-RO', + 'name' => 'English (Romania)', + ], + [ + 'code' => 'en-SK', + 'name' => 'English (Slovakia)', + ], + [ + 'code' => 'en-ZA', + 'name' => 'English (South Africa)', + ], + [ + 'code' => 'en-LK', + 'name' => 'English (Sri Lanka)', + ], + [ + 'code' => 'en-SE', + 'name' => 'English (Sweden)', + ], + [ + 'code' => 'en-CH', + 'name' => 'English (Switzerland)', + ], + [ + 'code' => 'en-GB', + 'name' => 'English (United Kingdom)', + ], + [ + 'code' => 'en-US', + 'name' => 'English (United States)', + ], + [ + 'code' => 'en-EN', + 'name' => 'English', + ], + [ + 'code' => 'myv', + 'name' => 'Erzya', + ], + [ + 'code' => 'eo', + 'name' => 'Esperanto', + ], + [ + 'code' => 'et', + 'name' => 'Estonian', + ], + [ + 'code' => 'et-EE', + 'name' => 'Estonian (Estonia)', + ], + [ + 'code' => 'fo', + 'name' => 'Faroese', + ], + [ + 'code' => 'fo-FO', + 'name' => 'Faroese (Faroe Islands)', + ], + [ + 'code' => 'fil', + 'name' => 'Filipino', + ], + [ + 'code' => 'fi', + 'name' => 'Finnish', + ], + [ + 'code' => 'fi-FI', + 'name' => 'Finnish (Finland)', + ], + [ + 'code' => 'frp', + 'name' => 'Franco-Provençal (Arpitan)', + ], + [ + 'code' => 'fr', + 'name' => 'French', + ], + [ + 'code' => 'fr-BE', + 'name' => 'French (Belgium)', + ], + [ + 'code' => 'fr-CA', + 'name' => 'French (Canada)', + ], + [ + 'code' => 'fr-FR', + 'name' => 'French (France)', + ], + [ + 'code' => 'fr-CH', + 'name' => 'French (Switzerland)', + ], + [ + 'code' => 'fur', + 'name' => 'Friulian', + ], + [ + 'code' => 'ff', + 'name' => 'Fulah', + ], + [ + 'code' => 'ff-SN', + 'name' => 'Fulah (Senegal)', + ], + [ + 'code' => 'gd', + 'name' => 'Gaelic, Scottish', + ], + [ + 'code' => 'gl', + 'name' => 'Galician', + ], + [ + 'code' => 'gl-ES', + 'name' => 'Galician (Spain)', + ], + [ + 'code' => 'lg', + 'name' => 'Ganda', + ], + [ + 'code' => 'ka', + 'name' => 'Georgian', + ], + [ + 'code' => 'ka-GE', + 'name' => 'Georgian (Georgia)', + ], + [ + 'code' => 'de', + 'name' => 'German', + ], + [ + 'code' => 'de-AT', + 'name' => 'German (Austria)', + ], + [ + 'code' => 'de-DE', + 'name' => 'German (Germany)', + ], + [ + 'code' => 'de-CH', + 'name' => 'German (Switzerland)', + ], + [ + 'code' => 'el', + 'name' => 'Greek', + ], + [ + 'code' => 'el-GR', + 'name' => 'Greek (Greece)', + ], + [ + 'code' => 'kl', + 'name' => 'Greenlandic', + ], + [ + 'code' => 'gu', + 'name' => 'Gujarati', + ], + [ + 'code' => 'gu-IN', + 'name' => 'Gujarati (India)', + ], + [ + 'code' => 'gun', + 'name' => 'Gun', + ], + [ + 'code' => 'ht', + 'name' => 'Haitian (Haitian Creole)', + ], + [ + 'code' => 'ht-HT', + 'name' => 'Haitian (Haitian Creole) (Haiti)', + ], + [ + 'code' => 'ha', + 'name' => 'Hausa', + ], + [ + 'code' => 'haw', + 'name' => 'Hawaiian', + ], + [ + 'code' => 'he', + 'name' => 'Hebrew', + ], + [ + 'code' => 'he-IL', + 'name' => 'Hebrew (Israel)', + ], + [ + 'code' => 'hi', + 'name' => 'Hindi', + ], + [ + 'code' => 'hi-IN', + 'name' => 'Hindi (India)', + ], + [ + 'code' => 'hu', + 'name' => 'Hungarian', + ], + [ + 'code' => 'hu-HU', + 'name' => 'Hungarian (Hungary)', + ], + [ + 'code' => 'is', + 'name' => 'Icelandic', + ], + [ + 'code' => 'is-IS', + 'name' => 'Icelandic (Iceland)', + ], + [ + 'code' => 'io', + 'name' => 'Ido', + ], + [ + 'code' => 'ig', + 'name' => 'Igbo', + ], + [ + 'code' => 'ilo', + 'name' => 'Iloko', + ], + [ + 'code' => 'id', + 'name' => 'Indonesian', + ], + [ + 'code' => 'id-ID', + 'name' => 'Indonesian (Indonesia)', + ], + [ + 'code' => 'ia', + 'name' => 'Interlingua', + ], + [ + 'code' => 'iu', + 'name' => 'Inuktitut', + ], + [ + 'code' => 'ga', + 'name' => 'Irish', + ], + [ + 'code' => 'ga-IE', + 'name' => 'Irish (Ireland)', + ], + [ + 'code' => 'it', + 'name' => 'Italian', + ], + [ + 'code' => 'it-IT', + 'name' => 'Italian (Italy)', + ], + [ + 'code' => 'it-CH', + 'name' => 'Italian (Switzerland)', + ], + [ + 'code' => 'ja', + 'name' => 'Japanese', + ], + [ + 'code' => 'ja-JP', + 'name' => 'Japanese (Japan)', + ], + [ + 'code' => 'jv', + 'name' => 'Javanese', + ], + [ + 'code' => 'kab', + 'name' => 'Kabyle', + ], + [ + 'code' => 'kn', + 'name' => 'Kannada', + ], + [ + 'code' => 'kn-IN', + 'name' => 'Kannada (India)', + ], + [ + 'code' => 'pam', + 'name' => 'Kapampangan', + ], + [ + 'code' => 'ks', + 'name' => 'Kashmiri', + ], + [ + 'code' => 'ks-IN', + 'name' => 'Kashmiri (India)', + ], + [ + 'code' => 'csb', + 'name' => 'Kashubian', + ], + [ + 'code' => 'kk', + 'name' => 'Kazakh', + ], + [ + 'code' => 'kk@Arab', + 'name' => 'Kazakh (Arabic)', + ], + [ + 'code' => 'kk@Cyrl', + 'name' => 'Kazakh (Cyrillic)', + ], + [ + 'code' => 'kk-KZ', + 'name' => 'Kazakh (Kazakhstan)', + ], + [ + 'code' => 'kk@latin', + 'name' => 'Kazakh (Latin)', + ], + [ + 'code' => 'km', + 'name' => 'Khmer', + ], + [ + 'code' => 'km-KH', + 'name' => 'Khmer (Cambodia)', + ], + [ + 'code' => 'rw', + 'name' => 'Kinyarwanda', + ], + [ + 'code' => 'ky', + 'name' => 'Kirgyz', + ], + [ + 'code' => 'tlh', + 'name' => 'Klingon', + ], + [ + 'code' => 'kok', + 'name' => 'Konkani', + ], + [ + 'code' => 'ko', + 'name' => 'Korean', + ], + [ + 'code' => 'ko-KR', + 'name' => 'Korean (Korea)', + ], + [ + 'code' => 'ku', + 'name' => 'Kurdish', + ], + [ + 'code' => 'ku-IQ', + 'name' => 'Kurdish (Iraq)', + ], + [ + 'code' => 'lad', + 'name' => 'Ladino', + ], + [ + 'code' => 'lo', + 'name' => 'Lao', + ], + [ + 'code' => 'lo-LA', + 'name' => 'Lao (Laos)', + ], + [ + 'code' => 'ltg', + 'name' => 'Latgalian', + ], + [ + 'code' => 'la', + 'name' => 'Latin', + ], + [ + 'code' => 'lv', + 'name' => 'Latvian', + ], + [ + 'code' => 'lv-LV', + 'name' => 'Latvian (Latvia)', + ], + [ + 'code' => 'lez', + 'name' => 'Lezghian', + ], + [ + 'code' => 'lij', + 'name' => 'Ligurian', + ], + [ + 'code' => 'li', + 'name' => 'Limburgian', + ], + [ + 'code' => 'ln', + 'name' => 'Lingala', + ], + [ + 'code' => 'lt', + 'name' => 'Lithuanian', + ], + [ + 'code' => 'lt-LT', + 'name' => 'Lithuanian (Lithuania)', + ], + [ + 'code' => 'jbo', + 'name' => 'Lojban', + ], + [ + 'code' => 'en@lolcat', + 'name' => 'LOLCAT English', + ], + [ + 'code' => 'lmo', + 'name' => 'Lombard', + ], + [ + 'code' => 'dsb', + 'name' => 'Lower Sorbian', + ], + [ + 'code' => 'nds', + 'name' => 'Low German', + ], + [ + 'code' => 'lb', + 'name' => 'Luxembourgish', + ], + [ + 'code' => 'mk', + 'name' => 'Macedonian', + ], + [ + 'code' => 'mk-MK', + 'name' => 'Macedonian (Macedonia)', + ], + [ + 'code' => 'mai', + 'name' => 'Maithili', + ], + [ + 'code' => 'mg', + 'name' => 'Malagasy', + ], + [ + 'code' => 'ms', + 'name' => 'Malay', + ], + [ + 'code' => 'ml', + 'name' => 'Malayalam', + ], + [ + 'code' => 'ml-IN', + 'name' => 'Malayalam (India)', + ], + [ + 'code' => 'ms-MY', + 'name' => 'Malay (Malaysia)', + ], + [ + 'code' => 'mt', + 'name' => 'Maltese', + ], + [ + 'code' => 'mt-MT', + 'name' => 'Maltese (Malta)', + ], + [ + 'code' => 'mni', + 'name' => 'Manipuri', + ], + [ + 'code' => 'mi', + 'name' => 'Maori', + ], + [ + 'code' => 'arn', + 'name' => 'Mapudungun', + ], + [ + 'code' => 'mr', + 'name' => 'Marathi', + ], + [ + 'code' => 'mr-IN', + 'name' => 'Marathi (India)', + ], + [ + 'code' => 'mh', + 'name' => 'Marshallese', + ], + [ + 'code' => 'mw1', + 'name' => 'Mirandese', + ], + [ + 'code' => 'mn', + 'name' => 'Mongolian', + ], + [ + 'code' => 'mn-MN', + 'name' => 'Mongolian (Mongolia)', + ], + [ + 'code' => 'nah', + 'name' => 'Nahuatl', + ], + [ + 'code' => 'nv', + 'name' => 'Navajo', + ], + [ + 'code' => 'nr', + 'name' => 'Ndebele, South', + ], + [ + 'code' => 'nap', + 'name' => 'Neapolitan', + ], + [ + 'code' => 'ne', + 'name' => 'Nepali', + ], + [ + 'code' => 'ne-NP', + 'name' => 'Nepali (Nepal)', + ], + [ + 'code' => 'nia', + 'name' => 'Nias', + ], + [ + 'code' => 'nqo', + 'name' => "N'ko", + ], + [ + 'code' => 'se', + 'name' => 'Northern Sami', + ], + [ + 'code' => 'nso', + 'name' => 'Northern Sotho', + ], + [ + 'code' => 'no', + 'name' => 'Norwegian', + ], + [ + 'code' => 'nb', + 'name' => 'Norwegian Bokmål', + ], + [ + 'code' => 'nb-NO', + 'name' => 'Norwegian Bokmål (Norway)', + ], + [ + 'code' => 'no-NO', + 'name' => 'Norwegian (Norway)', + ], + [ + 'code' => 'nn', + 'name' => 'Norwegian Nynorsk', + ], + [ + 'code' => 'nn-NO', + 'name' => 'Norwegian Nynorsk (Norway)', + ], + [ + 'code' => 'ny', + 'name' => 'Nyanja', + ], + [ + 'code' => 'oc', + 'name' => 'Occitan (post 1500)', + ], + [ + 'code' => 'or', + 'name' => 'Oriya', + ], + [ + 'code' => 'or-IN', + 'name' => 'Oriya (India)', + ], + [ + 'code' => 'om', + 'name' => 'Oromo', + ], + [ + 'code' => 'os', + 'name' => 'Ossetic', + ], + [ + 'code' => 'pfl', + 'name' => 'Palatinate German', + ], + [ + 'code' => 'pa', + 'name' => 'Panjabi (Punjabi)', + ], + [ + 'code' => 'pa-IN', + 'name' => 'Panjabi (Punjabi) (India)', + ], + [ + 'code' => 'pap', + 'name' => 'Papiamento', + ], + [ + 'code' => 'fa', + 'name' => 'Persian', + ], + [ + 'code' => 'fa-AF', + 'name' => 'Persian (Afghanistan)', + ], + [ + 'code' => 'fa-IR', + 'name' => 'Persian (Iran)', + ], + [ + 'code' => 'pms', + 'name' => 'Piemontese', + ], + [ + 'code' => 'en@pirate', + 'name' => 'Pirate English', + ], + [ + 'code' => 'pl', + 'name' => 'Polish', + ], + [ + 'code' => 'pl-PL', + 'name' => 'Polish (Poland)', + ], + [ + 'code' => 'pt', + 'name' => 'Portuguese', + ], + [ + 'code' => 'pt-BR', + 'name' => 'Portuguese (Brazil)', + ], + [ + 'code' => 'pt-PT', + 'name' => 'Portuguese (Portugal)', + ], + [ + 'code' => 'ps', + 'name' => 'Pushto', + ], + [ + 'code' => 'ro', + 'name' => 'Romanian', + ], + [ + 'code' => 'ro-RO', + 'name' => 'Romanian (Romania)', + ], + [ + 'code' => 'rm', + 'name' => 'Romansh', + ], + [ + 'code' => 'ru', + 'name' => 'Russian', + ], + [ + 'code' => 'ru-ee', + 'name' => 'Russian (Estonia)', + ], + [ + 'code' => 'ru-lv', + 'name' => 'Russian (Latvia)', + ], + [ + 'code' => 'ru-lt', + 'name' => 'Russian (Lithuania)', + ], + [ + 'code' => 'ru@petr1708', + 'name' => 'Russian Petrine orthography', + ], + [ + 'code' => 'ru-RU', + 'name' => 'Russian (Russia)', + ], + [ + 'code' => 'sah', + 'name' => 'Sakha (Yakut)', + ], + [ + 'code' => 'sm', + 'name' => 'Samoan', + ], + [ + 'code' => 'sa', + 'name' => 'Sanskrit', + ], + [ + 'code' => 'sat', + 'name' => 'Santali', + ], + [ + 'code' => 'sc', + 'name' => 'Sardinian', + ], + [ + 'code' => 'sco', + 'name' => 'Scots', + ], + [ + 'code' => 'sr', + 'name' => 'Serbian', + ], + [ + 'code' => 'sr@Ijekavian', + 'name' => 'Serbian (Ijekavian)', + ], + [ + 'code' => 'sr@ijekavianlatin', + 'name' => 'Serbian (Ijekavian Latin)', + ], + [ + 'code' => 'sr@latin', + 'name' => 'Serbian (Latin)', + ], + [ + 'code' => 'sr-RS@latin', + 'name' => 'Serbian (Latin) (Serbia)', + ], + [ + 'code' => 'sr-RS', + 'name' => 'Serbian (Serbia)', + ], + [ + 'code' => 'sn', + 'name' => 'Shona', + ], + [ + 'code' => 'scn', + 'name' => 'Sicilian', + ], + [ + 'code' => 'szl', + 'name' => 'Silesian', + ], + [ + 'code' => 'sd', + 'name' => 'Sindhi', + ], + [ + 'code' => 'si', + 'name' => 'Sinhala', + ], + [ + 'code' => 'si-LK', + 'name' => 'Sinhala (Sri Lanka)', + ], + [ + 'code' => 'sk', + 'name' => 'Slovak', + ], + [ + 'code' => 'sk-SK', + 'name' => 'Slovak (Slovakia)', + ], + [ + 'code' => 'sl', + 'name' => 'Slovenian', + ], + [ + 'code' => 'sl-SI', + 'name' => 'Slovenian (Slovenia)', + ], + [ + 'code' => 'so', + 'name' => 'Somali', + ], + [ + 'code' => 'son', + 'name' => 'Songhay', + ], + [ + 'code' => 'st', + 'name' => 'Sotho, Southern', + ], + [ + 'code' => 'st-ZA', + 'name' => 'Sotho, Southern (South Africa)', + ], + [ + 'code' => 'sma', + 'name' => 'Southern Sami', + ], + [ + 'code' => 'es', + 'name' => 'Spanish', + ], + [ + 'code' => 'es-AR', + 'name' => 'Spanish (Argentina)', + ], + [ + 'code' => 'es-BO', + 'name' => 'Spanish (Bolivia)', + ], + [ + 'code' => 'es-CL', + 'name' => 'Spanish (Chile)', + ], + [ + 'code' => 'es-CO', + 'name' => 'Spanish (Colombia)', + ], + [ + 'code' => 'es-CR', + 'name' => 'Spanish (Costa Rica)', + ], + [ + 'code' => 'es-DO', + 'name' => 'Spanish (Dominican Republic)', + ], + [ + 'code' => 'es-EC', + 'name' => 'Spanish (Ecuador)', + ], + [ + 'code' => 'es-SV', + 'name' => 'Spanish (El Salvador)', + ], + [ + 'code' => 'es-GT', + 'name' => 'Spanish (Guatemala)', + ], + [ + 'code' => 'es-419', + 'name' => 'Spanish (Latin America)', + ], + [ + 'code' => 'es-MX', + 'name' => 'Spanish (Mexico)', + ], + [ + 'code' => 'es-NI', + 'name' => 'Spanish (Nicaragua)', + ], + [ + 'code' => 'es-PA', + 'name' => 'Spanish (Panama)', + ], + [ + 'code' => 'es-PY', + 'name' => 'Spanish (Paraguay)', + ], + [ + 'code' => 'es-PE', + 'name' => 'Spanish (Peru)', + ], + [ + 'code' => 'es-PR', + 'name' => 'Spanish (Puerto Rico)', + ], + [ + 'code' => 'es-ES', + 'name' => 'Spanish (Spain)', + ], + [ + 'code' => 'es-US', + 'name' => 'Spanish (United States)', + ], + [ + 'code' => 'es-UY', + 'name' => 'Spanish (Uruguay)', + ], + [ + 'code' => 'es-VE', + 'name' => 'Spanish (Venezuela)', + ], + [ + 'code' => 'su', + 'name' => 'Sundanese', + ], + [ + 'code' => 'sw', + 'name' => 'Swahili', + ], + [ + 'code' => 'sw-KE', + 'name' => 'Swahili (Kenya)', + ], + [ + 'code' => 'ss', + 'name' => 'Swati', + ], + [ + 'code' => 'sv', + 'name' => 'Swedish', + ], + [ + 'code' => 'sv-FI', + 'name' => 'Swedish (Finland)', + ], + [ + 'code' => 'sv-SE', + 'name' => 'Swedish (Sweden)', + ], + [ + 'code' => 'tl', + 'name' => 'Tagalog', + ], + [ + 'code' => 'tl-PH', + 'name' => 'Tagalog (Philippines)', + ], + [ + 'code' => 'tg', + 'name' => 'Tajik', + ], + [ + 'code' => 'tg-TJ', + 'name' => 'Tajik (Tajikistan)', + ], + [ + 'code' => 'tzl', + 'name' => 'Talossan', + ], + [ + 'code' => 'ta', + 'name' => 'Tamil', + ], + [ + 'code' => 'ta-IN', + 'name' => 'Tamil (India)', + ], + [ + 'code' => 'ta-LK', + 'name' => 'Tamil (Sri-Lanka)', + ], + [ + 'code' => 'tt', + 'name' => 'Tatar', + ], + [ + 'code' => 'te', + 'name' => 'Telugu', + ], + [ + 'code' => 'te-IN', + 'name' => 'Telugu (India)', + ], + [ + 'code' => 'tet', + 'name' => 'Tetum (Tetun)', + ], + [ + 'code' => 'th', + 'name' => 'Thai', + ], + [ + 'code' => 'th-TH', + 'name' => 'Thai (Thailand)', + ], + [ + 'code' => 'bo', + 'name' => 'Tibetan', + ], + [ + 'code' => 'bo-CN', + 'name' => 'Tibetan (China)', + ], + [ + 'code' => 'ti', + 'name' => 'Tigrinya', + ], + [ + 'code' => 'to', + 'name' => 'Tongan', + ], + [ + 'code' => 'ts', + 'name' => 'Tsonga', + ], + [ + 'code' => 'tn', + 'name' => 'Tswana', + ], + [ + 'code' => 'tr', + 'name' => 'Turkish', + ], + [ + 'code' => 'tr-TR', + 'name' => 'Turkish (Turkey)', + ], + [ + 'code' => 'tk', + 'name' => 'Turkmen', + ], + [ + 'code' => 'tk-TM', + 'name' => 'Turkmen (Turkmenistan)', + ], + [ + 'code' => 'udm', + 'name' => 'Udmurt', + ], + [ + 'code' => 'ug', + 'name' => 'Uighur', + ], + [ + 'code' => 'ug@Arab', + 'name' => 'Uighur (Arabic)', + ], + [ + 'code' => 'ug@Cyrl', + 'name' => 'Uighur (Cyrillic)', + ], + [ + 'code' => 'ug@Latin', + 'name' => 'Uighur (Latin)', + ], + [ + 'code' => 'uk', + 'name' => 'Ukrainian', + ], + [ + 'code' => 'uk-UA', + 'name' => 'Ukrainian (Ukraine)', + ], + [ + 'code' => 'vmf', + 'name' => 'Upper Franconian', + ], + [ + 'code' => 'hsb', + 'name' => 'Upper Sorbian', + ], + [ + 'code' => 'ur', + 'name' => 'Urdu', + ], + [ + 'code' => 'ur-PK', + 'name' => 'Urdu (Pakistan)', + ], + [ + 'code' => 'uz', + 'name' => 'Uzbek', + ], + [ + 'code' => 'uz@Arab', + 'name' => 'Uzbek (Arabic)', + ], + [ + 'code' => 'uz@Cyrl', + 'name' => 'Uzbek (Cyrillic)', + ], + [ + 'code' => 'uz@Latn', + 'name' => 'Uzbek (Latin)', + ], + [ + 'code' => 'uz-UZ', + 'name' => 'Uzbek (Uzbekistan)', + ], + [ + 'code' => 've', + 'name' => 'Venda', + ], + [ + 'code' => 'vec', + 'name' => 'Venetian', + ], + [ + 'code' => 'vi', + 'name' => 'Vietnamese', + ], + [ + 'code' => 'vi-VN', + 'name' => 'Vietnamese (Viet Nam)', + ], + [ + 'code' => 'vls', + 'name' => 'Vlaams', + ], + [ + 'code' => 'wa', + 'name' => 'Walloon', + ], + [ + 'code' => 'war', + 'name' => 'Wáray-Wáray', + ], + [ + 'code' => 'cy', + 'name' => 'Welsh', + ], + [ + 'code' => 'cy-GB', + 'name' => 'Welsh (United Kingdom)', + ], + [ + 'code' => 'fy', + 'name' => 'Western Frisian', + ], + [ + 'code' => 'fy-NL', + 'name' => 'Western Frisian (Netherlands)', + ], + [ + 'code' => 'wo', + 'name' => 'Wolof', + ], + [ + 'code' => 'wo-SN', + 'name' => 'Wolof (Senegal)', + ], + [ + 'code' => 'xh', + 'name' => 'Xhosa', + ], + [ + 'code' => 'yi', + 'name' => 'Yiddish', + ], + [ + 'code' => 'yo', + 'name' => 'Yoruba', + ], + [ + 'code' => 'zu', + 'name' => 'Zulu', + ], + [ + 'code' => 'zu-ZA', + 'name' => 'Zulu (South Africa)', + ], ]; return response()->json(['results' => $languages]); - } -} + }//end index() +}//end class diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 87709f585b..5468d333a8 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -1,6 +1,7 @@ [ 'error' => 404, 'message' => 'Not found' ] ], - 404); + 404 + ); } return new SurveyResource($survey); } @@ -55,7 +56,8 @@ public function index() * @return SurveyResource * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function store(Request $request) { + public function store(Request $request) + { $authorizer = service('authorizer.form'); // if there's no user the guards will kick them off already, but if there // is one we need to check the authorizer to ensure we don't let @@ -69,7 +71,8 @@ public function store(Request $request) { $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey = Survey::create( array_merge( - $request->input(),[ 'updated' => time(), 'created' => time()] + $request->input(), + [ 'updated' => time(), 'created' => time()] ) ); $this->saveTranslations($request->input('translations'), $survey->id, 'survey'); @@ -77,7 +80,8 @@ public function store(Request $request) { foreach ($request->input('tasks') as $stage) { $stage_model = $survey->tasks()->create( array_merge( - $stage, [ 'updated' => time(), 'created' => time()] + $stage, + [ 'updated' => time(), 'created' => time()] ) ); $this->saveTranslations($stage['translations'] ?? [], $stage_model->id, 'task'); @@ -86,7 +90,8 @@ public function store(Request $request) { $attribute['key'] = $uuid->toString(); $field_model = $stage_model->fields()->create( array_merge( - $attribute, [ 'updated' => time(), 'created' => time()] + $attribute, + [ 'updated' => time(), 'created' => time()] ) ); $this->saveTranslations($attribute['translations'] ?? [], $field_model->id, 'field'); @@ -102,13 +107,14 @@ public function store(Request $request) { * @param $type * @return bool */ - private function saveTranslations($input, int $translatable_id, string $type) { + private function saveTranslations($input, int $translatable_id, string $type) + { if (!is_array($input)) { return true; } foreach ($input as $language => $translations) { foreach ($translations as $key => $translated) { - if (is_array($translated)){ + if (is_array($translated)) { $translated = json_encode($translated); } Translation::create([ @@ -128,7 +134,8 @@ private function saveTranslations($input, int $translatable_id, string $type) { * @param $type * @return bool */ - private function updateTranslations($input, int $translatable_id, string $type) { + private function updateTranslations($input, int $translatable_id, string $type) + { if (!is_array($input)) { return true; } @@ -137,7 +144,7 @@ private function updateTranslations($input, int $translatable_id, string $type) ->delete(); foreach ($input as $language => $translations) { foreach ($translations as $key => $translated) { - if (is_array($translated)){ + if (is_array($translated)) { $translated = json_encode($translated); } Translation::create([ @@ -158,14 +165,16 @@ private function updateTranslations($input, int $translatable_id, string $type) * @return mixed * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function update(int $id, Request $request) { + public function update(int $id, Request $request) + { $survey = Survey::find($id); if (!$survey) { return response()->json( [ 'errors' => [ 'error' => 404, 'message' => 'Not found' ] ], - 404); + 404 + ); } $this->authorize('update', $survey); if (!$survey) { @@ -173,12 +182,14 @@ public function update(int $id, Request $request) { [ 'errors' => [ 'error' => 404, 'message' => 'Not found' ] ], - 404); + 404 + ); } $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey->update( array_merge( - $request->input(),[ 'updated' => time()] + $request->input(), + [ 'updated' => time()] ) ); $this->updateTranslations($request->input('translations'), $survey->id, 'survey'); @@ -190,19 +201,21 @@ public function update(int $id, Request $request) { * @param array $input_tasks * @param Survey $survey */ - private function updateTasks(array $input_tasks, Survey $survey) { + private function updateTasks(array $input_tasks, Survey $survey) + { $added_tasks = []; foreach ($input_tasks as $stage) { if (isset($stage['id'])) { $stage_model = $survey->tasks->find($stage['id']); - if (!$stage_model){ + if (!$stage_model) { continue; } $stage_model->update($stage); $stage_model = Stage::find($stage['id']); } else { $stage_model = $survey->tasks()->create(array_merge( - $stage, [ 'updated' => time()] + $stage, + [ 'updated' => time()] )); $added_tasks[] = $stage_model->id; } @@ -213,10 +226,11 @@ private function updateTasks(array $input_tasks, Survey $survey) { $survey->load('tasks'); $tasks_to_delete = $survey->tasks->whereNotIn( - 'id',array_merge($added_tasks, $input_tasks_collection->groupBy('id')->keys()->toArray()) + 'id', + array_merge($added_tasks, $input_tasks_collection->groupBy('id')->keys()->toArray()) ); foreach ($tasks_to_delete as $task_to_delete) { - Stage::where('id',$task_to_delete->id)->delete(); + Stage::where('id', $task_to_delete->id)->delete(); } } @@ -225,12 +239,13 @@ private function updateTasks(array $input_tasks, Survey $survey) { * @param Survey $survey * @param Stage $stage */ - private function updateFields(array $input_fields, Stage $stage) { + private function updateFields(array $input_fields, Stage $stage) + { $added_fields = []; foreach ($input_fields as $field) { if (isset($field['id'])) { $field_model = $stage->fields->find($field['id']); - if (!$field_model){ + if (!$field_model) { continue; } $field_model->update($field); @@ -238,7 +253,8 @@ private function updateFields(array $input_fields, Stage $stage) { } else { $uuid = Uuid::uuid4(); $field_model = $stage->fields()->create(array_merge( - $field, [ 'updated' => time(), 'key' => $uuid->toString()] + $field, + [ 'updated' => time(), 'key' => $uuid->toString()] )); $added_fields[] = $field_model->id; } @@ -248,16 +264,18 @@ private function updateFields(array $input_fields, Stage $stage) { $stage->load('fields'); $fields_to_delete = $stage->fields->whereNotIn( - 'id',array_merge($added_fields, $input_fields_collection->groupBy('id')->keys()->toArray()) + 'id', + array_merge($added_fields, $input_fields_collection->groupBy('id')->keys()->toArray()) ); foreach ($fields_to_delete as $field_to_delete) { - Attribute::where('id',$field_to_delete->id)->delete(); + Attribute::where('id', $field_to_delete->id)->delete(); } } /** * @param int $id */ - public function delete(int $id, Request $request) { + public function delete(int $id, Request $request) + { $survey = Survey::find($id); $this->authorize('delete', $survey); return response()->json(['result' => ['deleted' => $id]]); diff --git a/v4/Http/Resources/FieldCollection.php b/v4/Http/Resources/FieldCollection.php index c679e83968..7f69a4db15 100644 --- a/v4/Http/Resources/FieldCollection.php +++ b/v4/Http/Resources/FieldCollection.php @@ -3,7 +3,6 @@ namespace v4\Http\Resources; - use Illuminate\Http\Resources\Json\ResourceCollection; class FieldCollection extends ResourceCollection diff --git a/v4/Http/Resources/FieldResource.php b/v4/Http/Resources/FieldResource.php index ca70111ed9..d1e5556ba9 100644 --- a/v4/Http/Resources/FieldResource.php +++ b/v4/Http/Resources/FieldResource.php @@ -1,5 +1,6 @@ collection->mapToGroups(function ($item, $key) { - if ( - $item->translated_key === 'options' && + if ($item->translated_key === 'options' && $item->translatable_type==='field' ) { $item->translation = json_decode($item->translation); @@ -34,7 +32,7 @@ public function toArray($request) return [$item->language => $item]; }); $combined = $grouped->map(function ($item, $key) { - return $item->mapWithKeys(function($i) { + return $item->mapWithKeys(function ($i) { return [$i->translated_key => $i->translation ]; }); }); diff --git a/v4/Http/Resources/TranslationResource.php b/v4/Http/Resources/TranslationResource.php index c3d561a8d2..7375bcb227 100644 --- a/v4/Http/Resources/TranslationResource.php +++ b/v4/Http/Resources/TranslationResource.php @@ -1,5 +1,6 @@ 'json', ]; - public function stage () { + public function stage() + { return $this->belongsTo('v4\Models\Stage', 'form_stage_id'); } diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index 43aad7655b..91ddb2e809 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -7,7 +7,7 @@ class Stage extends Model { - public $timestamps = FALSE; + public $timestamps = false; protected $table = 'form_stages'; /** @@ -38,7 +38,8 @@ public function fields() return $this->hasMany('v4\Models\Attribute', 'form_stage_id'); } - public function survey() { + public function survey() + { return $this->belongsTo('v4\Models\Survey', 'form_id'); } @@ -49,5 +50,4 @@ public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); } - } diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index bd7acb58ac..1272914bb7 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -12,28 +12,53 @@ class Survey extends Model { use InteractsWithFormPermissions; + + /** + * Add eloquent style timestamps + * + * @var boolean + */ + public $timestamps = false; + + /** + * Specify the table to load with Survey + * + * @var string + */ protected $table = 'forms'; + + /** + * Add relations to eager load + * + * @var string[] + */ protected $with = ['tasks']; - public $timestamps = FALSE; + /** * The attributes that should be hidden for serialization. + * + * @var array * @note this should be changed so that we either use the fractal transformer * OR a policy authorizer which is a more or less accepted method to do it * (which uses the same $hidden type thing but it's much nicer obviously) - * - * @var array */ protected $hidden = ['description']; + /** * The attributes that should be mutated to dates. + * * @var array - */ - protected $dates = ['created', 'updated']; + */ + protected $dates = [ + 'created', + 'updated', + ]; /** - * The attributes that are mass assignable. - * @var array - */ + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'parent_id', 'name', @@ -47,7 +72,7 @@ class Survey extends Model 'hide_time', 'hide_location', 'targeted_survey', - 'base_language' + 'base_language', ]; /** @@ -56,238 +81,328 @@ class Survey extends Model * @var array */ protected $appends = ['can_create']; + /** * The model's default values for attributes. * * @var array */ protected $attributes = [ - 'type' => 'report', - 'require_approval' => true, + 'type' => 'report', + 'require_approval' => true, 'everyone_can_create' => true, - 'hide_author' => false, - 'hide_time' => false, - 'disabled' => false, - 'hide_location' => false, - 'targeted_survey' => false + 'hide_author' => false, + 'hide_time' => false, + 'disabled' => false, + 'hide_location' => false, + 'targeted_survey' => false, ]; /** - * This is what makes can_create possible - * @return mixed + * The attributes that should be cast. + * + * @var array */ - public function getCanCreateAttribute() { - $can_create = $this->getCanCreateRoles($this->id); - return $can_create['roles']; - } + protected $casts = [ + 'everyone_can_create' => 'boolean', + 'hide_author' => 'boolean', + 'require_approval' => 'boolean', + 'disabled' => 'boolean', + ]; + - private function getCanCreateRoles($form_id) { - /** - * @NOTE: to lower changes of a regression I'm using some helpers from - * repositories and traits we already have - * @NOTE: during origami and later stages of sunny buffers, we will fold this - * all together in more performant and friendly ways - */ - $form_repo = service('repository.form'); - return $form_repo->getRolesThatCanCreatePosts($form_id); - } /** - * We check for relationship permissions here, to avoid hydrating anything that should not be hydrated. - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * Get the error messages for the defined validation rules. + * + * @return array */ - public function tasks() + public static function validationMessages() { - $authorizer = service('authorizer.form'); - $user = $authorizer->getUser(); - // NOTE: this acl->hasPermission check is all `canUserEditForm` does, so we're doing that directly - // to avoid an hydration issue with InteractsWithFormPermissions - if ($authorizer->acl->hasPermission($user, Permission::MANAGE_POSTS)) { - // if this permission is set we can go ahead and hydrate all the stages - return $this->hasMany('v4\Models\Stage', 'form_id'); - } - return $this->hasMany('v4\Models\Stage', 'form_id') - ->where('form_stages.show_when_published', '=', '1') - ->where('form_stages.task_is_internal_only', '=', '0'); - } + return [ + 'name.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.name')] + ), + 'name.min' => trans( + 'validation.min_length', + ['param2' => 2] + ), + 'name.max' => trans( + 'validation.max_length', + ['param2' => 255] + ), + 'name.regex' => trans( + 'validation.regex', + ['field' => trans('fields.name')] + ), + // @TODO Add description.string + // @TODO Add color.string + 'disabled.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.disabled')] + ), + 'everyone_can_create.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.everyone_can_create')] + ), + 'hide_author.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.hide_author')] + ), + 'hide_location.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.hide_location')] + ), + 'hide_time.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.hide_time')] + ), + 'targeted_survey.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.targeted_survey')] + ), + 'tasks.*.label.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.tasks.label')] + ), + 'tasks.*.label.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.tasks.label')] + ), + 'tasks.*.type.in' => trans( + 'validation.in_array', + ['field' => trans('fields.tasks.type')] + ), + 'tasks.*.priority.numeric' => trans( + 'validation.numeric', + ['field' => trans('fields.tasks.priority')] + ), + 'tasks.*.icon.alpha' => trans( + 'validation.alpha', + ['field' => trans('fields.tasks.icon')] + ), + 'tasks.*.fields.*.label.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.tasks.fields.label')] + ), + 'tasks.*.fields.*.label.max' => trans( + 'validation.max_length', + ['param2' => trans('fields.tasks.fields.label')] + ), + 'tasks.*.fields.*.key.alpha_dash' => trans( + 'validation.alpha_dash', + ['field' => trans('fields.tasks.fields.key')] + ), + 'tasks.*.fields.*.key.max' => trans( + 'validation.max_length', + ['param2' => trans('fields.tasks.fields.key')] + ), + 'tasks.*.fields.*.input.required' => trans( + 'validation.not_empty', + ['param2' => trans('fields.tasks.fields.input')] + ), + 'tasks.*.fields.*.input.in' => trans( + 'validation.in_array', + ['param2' => trans('fields.tasks.fields.input')] + ), + 'tasks.*.fields.*.type.required' => trans( + 'validation.not_empty', + ['param2' => trans('fields.tasks.fields.type')] + ), + 'tasks.*.fields.*.type.in' => trans( + 'validation.in_array', + ['param2' => trans('fields.tasks.fields.type')] + ), + 'tasks.*.fields.*.priority.numeric' => trans( + 'validation.numeric', + ['param2' => trans('fields.tasks.fields.priority')] + ), + 'tasks.*.fields.*.cardinality.numeric' => trans( + 'validation.numeric', + ['param2' => trans('fields.tasks.fields.cardinality')] + ), + 'tasks.*.fields.*.response_private.boolean' => trans( + 'validation.regex', + ['field' => trans('fields.tasks.fields.response_private')] + ), + // 'tasks.*.fields.*.response_private' => [ + // @TODO add this custom validator for canMakePrivate + // [[$this, 'canMakePrivate'], [':value', $type]] + // ] + ]; + }//end validationMessages() - protected static function getRules() { + + /** + * Return all validation rules + * + * @return array + */ + protected static function getRules() + { return [ - 'name' => [ + 'name' => [ 'required', 'min:2', 'max:255', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT, ], - 'description' => [ + 'description' => [ 'string', - 'nullable' + 'nullable', ], - //@TODO find out where this color validator is implemented - //[['color']], - 'color' => [ + // @TODO find out where this color validator is implemented + // [['color']], + 'color' => [ 'string', - 'nullable' - ], - 'disabled' => [ - 'boolean' + 'nullable', ], - 'everyone_can_create' => [ - 'boolean' - ], - 'hide_author' => [ - 'boolean' - ], - 'hide_location' => [ - 'boolean' - ], - 'hide_time' => [ - 'boolean' - ], - 'targeted_survey' => [ - 'boolean' - ], - 'tasks.*.label' => [ + 'disabled' => ['boolean'], + 'everyone_can_create' => ['boolean'], + 'hide_author' => ['boolean'], + 'hide_location' => ['boolean'], + 'hide_time' => ['boolean'], + 'targeted_survey' => ['boolean'], + 'tasks.*.label' => [ 'required', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT - ], - 'tasks.*.type' => [ - Rule::in(['post', 'task']) + 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT, ], - 'tasks.*.priority' => [ - 'numeric', + 'tasks.*.type' => [ + Rule::in( + [ + 'post', + 'task', + ] + ) ], - 'tasks.*.icon' => [ - 'alpha', - ], - 'tasks.*.fields.*.label' => [ + 'tasks.*.priority' => ['numeric'], + 'tasks.*.icon' => ['alpha'], + 'tasks.*.fields.*.label' => [ 'required', - 'max:150' + 'max:150', ], - 'tasks.*.fields.*.key' => [ + 'tasks.*.fields.*.key' => [ 'max:150', - 'alpha_dash' + 'alpha_dash', // @TODO: add this validation for keys - //[[$this->repo, 'isKeyAvailable'], [':value']] + // [[$this->repo, 'isKeyAvailable'], [':value']] ], - 'tasks.*.fields.*.input' => [ + 'tasks.*.fields.*.input' => [ 'required', - Rule::in([ - 'text', - 'textarea', - 'select', - 'radio', - 'checkbox', - 'checkboxes', - 'date', - 'datetime', - 'location', - 'number', - 'relation', - 'upload', - 'video', - 'markdown', - 'tags', - ]) + Rule::in( + [ + 'text', + 'textarea', + 'select', + 'radio', + 'checkbox', + 'checkboxes', + 'date', + 'datetime', + 'location', + 'number', + 'relation', + 'upload', + 'video', + 'markdown', + 'tags', + ] + ), ], - 'tasks.*.fields.*.type' => [ + 'tasks.*.fields.*.type' => [ 'required', - Rule::in([ - 'decimal', - 'int', - 'geometry', - 'text', - 'varchar', - 'markdown', - 'point', - 'datetime', - 'link', - 'relation', - 'media', - 'title', - 'description', - 'tags', - ]) + Rule::in( + [ + 'decimal', + 'int', + 'geometry', + 'text', + 'varchar', + 'markdown', + 'point', + 'datetime', + 'link', + 'relation', + 'media', + 'title', + 'description', + 'tags', + ] + ), // @TODO: add this validation for duplicates in type? - //[[$this, 'checkForDuplicates'], [':validation', ':value']], - ], - 'tasks.*.fields.*.type' => [ - 'string' - ], - 'tasks.*.fields.*.priority' => [ - 'numeric', - ], - 'tasks.*.fields.*.cardinality' => [ - 'numeric', + // [[$this, 'checkForDuplicates'], [':validation', ':value']], ], + 'tasks.*.fields.*.type' => ['string'], + 'tasks.*.fields.*.priority' => ['numeric'], + 'tasks.*.fields.*.cardinality' => ['numeric'], 'tasks.*.fields.*.response_private' => [ 'boolean' // @TODO add this custom validator for canMakePrivate // [[$this, 'canMakePrivate'], [':value', $type]] - ] + ], // @NOTE: checkPostTypeLimit is not used here. // Before merge, validate with Angela if we // should be removing that arbitrary limit since it's pretty rare // for it to be needed ]; - } + }//end getRules() + + /** - * Get the error messages for the defined validation rules. + * This is what makes can_create possible * - * @return array + * @return mixed */ - public static function validationMessages() + public function getCanCreateAttribute() { - return [ - 'name.required' => trans('validation.not_empty', ['field' => trans('fields.name')]), - 'name.min' => trans('validation.min_length', ['param2' => 2]), - 'name.max' => trans('validation.max_length', ['param2' => 255]), - 'name.regex' => trans('validation.regex', ['field' => trans('fields.name')]), - //description.string - //color.string - 'disabled.boolean' => trans('validation.regex', ['field' => trans('fields.disabled')]), - 'everyone_can_create.boolean' => trans('validation.regex', ['field' => trans('fields.everyone_can_create')]), - 'hide_author.boolean' => trans('validation.regex', ['field' => trans('fields.hide_author')]), - 'hide_location.boolean' => trans('validation.regex', ['field' => trans('fields.hide_location')]), - 'hide_time.boolean' => trans('validation.regex', ['field' => trans('fields.hide_time')]), - 'targeted_survey.boolean' => trans('validation.regex', ['field' => trans('fields.targeted_survey')]), - 'tasks.*.label.required' => trans('validation.not_empty', ['field' => trans('fields.tasks.label')]), - 'tasks.*.label.boolean' => trans('validation.regex', ['field' => trans('fields.tasks.label')]), - 'tasks.*.type.in' => trans('validation.in_array', ['field' => trans('fields.tasks.type')]), - 'tasks.*.priority.numeric' => trans('validation.numeric', ['field' => trans('fields.tasks.priority')]), - 'tasks.*.icon.alpha' => trans('validation.alpha', ['field' => trans('fields.tasks.icon')]), - 'tasks.*.fields.*.label.required' => trans('validation.not_empty', ['field' => trans('fields.tasks.fields.label')]), - 'tasks.*.fields.*.label.max' => trans('validation.max_length', ['param2' => trans('fields.tasks.fields.label')]), - 'tasks.*.fields.*.key.alpha_dash' => trans('validation.alpha_dash', ['field' => trans('fields.tasks.fields.key')]), - 'tasks.*.fields.*.key.max' => trans('validation.max_length', ['param2' => trans('fields.tasks.fields.key')]), - 'tasks.*.fields.*.input.required' => trans('validation.not_empty', ['param2' => trans('fields.tasks.fields.input')]), - 'tasks.*.fields.*.input.in' => trans('validation.in_array', ['param2' => trans('fields.tasks.fields.input')]), - 'tasks.*.fields.*.type.required' => trans('validation.not_empty', ['param2' => trans('fields.tasks.fields.type')]), - 'tasks.*.fields.*.type.in' => trans('validation.in_array', ['param2' => trans('fields.tasks.fields.type')]), - 'tasks.*.fields.*.priority.numeric' => trans('validation.numeric', ['param2' => trans('fields.tasks.fields.priority')]), - 'tasks.*.fields.*.cardinality.numeric' => trans('validation.numeric', ['param2' => trans('fields.tasks.fields.cardinality')]), - 'tasks.*.fields.*.response_private.boolean' => trans('validation.regex', ['field' => trans('fields.tasks.fields.response_private')]), - //'tasks.*.fields.*.response_private' => [ - //// @TODO add this custom validator for canMakePrivate - // [[$this, 'canMakePrivate'], [':value', $type]] - //] - ]; - } + $can_create = $this->getCanCreateRoles($this->id); + return $can_create['roles']; + }//end getCanCreateAttribute() + + + private function getCanCreateRoles($form_id) + { + /* + * @NOTE: to lower changes of a regression I'm using some helpers from + * repositories and traits we already have + * @NOTE: during origami and later stages of sunny buffers, we will fold this + * all together in more performant and friendly ways + */ + $form_repo = service('repository.form'); + return $form_repo->getRolesThatCanCreatePosts($form_id); + }//end getCanCreateRoles() + + + /** + * We check for relationship permissions here, to avoid hydrating anything that should not be hydrated. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function tasks() + { + $authorizer = service('authorizer.form'); + $user = $authorizer->getUser(); + // NOTE: this acl->hasPermission check is all `canUserEditForm` does, so we're doing that directly + // to avoid an hydration issue with InteractsWithFormPermissions + if ($authorizer->acl->hasPermission($user, Permission::MANAGE_POSTS)) { + // if this permission is set we can go ahead and hydrate all the stages + return $this->hasMany('v4\Models\Stage', 'form_id'); + } + + return $this->hasMany( + 'v4\Models\Stage', + 'form_id' + ) + ->where('form_stages.show_when_published', '=', '1') + ->where('form_stages.task_is_internal_only', '=', '0'); + }//end tasks() + + /** * Get the survey's translation. */ public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); - } - /** - * The attributes that should be cast. - * - * @var array - */ - protected $casts = [ - 'everyone_can_create' => 'boolean', - 'hide_author' => 'boolean', - 'require_approval' => 'boolean', - 'disabled' => 'boolean' - ]; -} + }//end translations() +}//end class diff --git a/v4/Models/Translation.php b/v4/Models/Translation.php index ad60f53f3b..8c6ff503b8 100644 --- a/v4/Models/Translation.php +++ b/v4/Models/Translation.php @@ -7,7 +7,7 @@ class Translation extends Model { - public $timestamps = FALSE; + public $timestamps = false; protected $table = 'translations'; /** diff --git a/v4/Policies/SurveyPolicy.php b/v4/Policies/SurveyPolicy.php index cd1c5aee0c..c6b8b50b0e 100644 --- a/v4/Policies/SurveyPolicy.php +++ b/v4/Policies/SurveyPolicy.php @@ -1,6 +1,7 @@ toArray()); return $this->isAllowed($form, 'update'); @@ -85,7 +87,8 @@ public function update(User $user, Survey $survey) { * @param Survey $survey * @return bool */ - public function store() { + public function store() + { // we convert to a form entity to be able to continue using the old authorizers and classes. $form = new Entity\Form(); return $this->isAllowed($form, 'create'); @@ -95,7 +98,8 @@ public function store() { * @param string $privilege * @return bool */ - public function isAllowed($entity, $privilege){ + public function isAllowed($entity, $privilege) + { $authorizer = service('authorizer.form'); // These checks are run within the user context. @@ -124,9 +128,9 @@ public function isAllowed($entity, $privilege){ // } // If a form is not disabled, then *anyone* can view it. - if ($privilege === 'read' && !$this->isFormDisabled($entity)) { - return true; - } + if ($privilege === 'read' && !$this->isFormDisabled($entity)) { + return true; + } // All users are allowed to search forms. // @TODO should only do 'search' here. Do 'read' above in the isFormDisabled check @@ -146,5 +150,4 @@ protected function isFormDisabled(Entity\Form $entity) { return (bool) $entity->disabled; } - } diff --git a/v4/Providers/MorphServiceProvider.php b/v4/Providers/MorphServiceProvider.php index a8a20b00c3..231b6b3b51 100644 --- a/v4/Providers/MorphServiceProvider.php +++ b/v4/Providers/MorphServiceProvider.php @@ -8,7 +8,8 @@ class MorphServiceProvider extends ServiceProvider { - public function boot() { + public function boot() + { Relation::morphMap([ 'survey' => 'v4\Models\Survey', 'task' => 'v4\Models\Stage', From 33a3ce2eb4b18693e03f1afa73a7dcb6a510f65e Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 12 May 2020 00:27:13 -0300 Subject: [PATCH 27/39] Hide icon --- v4/Http/Resources/TaskResource.php | 1 - v4/Models/Attribute.php | 1 + v4/Models/Stage.php | 1 + v4/Models/Survey.php | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/v4/Http/Resources/TaskResource.php b/v4/Http/Resources/TaskResource.php index 3e599b1497..2f96b40b62 100644 --- a/v4/Http/Resources/TaskResource.php +++ b/v4/Http/Resources/TaskResource.php @@ -20,7 +20,6 @@ public function toArray($request) 'form_id' => $this->form_id, 'label' => $this->label, 'priority' => $this->priority, - 'icon' => $this->icon, 'required' => $this->required, 'type' => $this->type, 'description' => $this->description, diff --git a/v4/Models/Attribute.php b/v4/Models/Attribute.php index ca8e16f1fa..b645eefc13 100644 --- a/v4/Models/Attribute.php +++ b/v4/Models/Attribute.php @@ -41,6 +41,7 @@ class Attribute extends Model 'config' => 'json', 'options' => 'json', ]; + protected $hidden = ['icon']; public function stage() { diff --git a/v4/Models/Stage.php b/v4/Models/Stage.php index 91ddb2e809..17185bb2fa 100644 --- a/v4/Models/Stage.php +++ b/v4/Models/Stage.php @@ -32,6 +32,7 @@ class Stage extends Model 'show_when_published', 'task_is_internal_only' ]; + protected $hidden = ['icon']; public function fields() { diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 1272914bb7..28bd802062 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -42,7 +42,7 @@ class Survey extends Model * OR a policy authorizer which is a more or less accepted method to do it * (which uses the same $hidden type thing but it's much nicer obviously) */ - protected $hidden = ['description']; + protected $hidden = ['description', 'icon']; /** * The attributes that should be mutated to dates. From d83069d4634626066c151055d6260d7c563e3fe7 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 12 May 2020 01:55:23 -0300 Subject: [PATCH 28/39] Delete surveys with translations,tasks,fields --- tests/integration/v4/forms/forms.v4.feature | 2 +- v4/Http/Controllers/SurveyController.php | 233 +++++++++++++------- 2 files changed, 154 insertions(+), 81 deletions(-) diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index cf519e92b8..b47802b6fc 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -602,7 +602,7 @@ Feature: Testing the Surveys API Scenario: Finding a non-existent Survey Given that I want to find a "Survey" And that the api_url is "api/v4" - And that its "id" is "1332" + And that its "id" is "1" When I request "/surveys" Then the response is JSON And the response has a "errors" property diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 5468d333a8..0ac7160559 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -18,10 +18,11 @@ class SurveyController extends V4Controller { + /** * Display the specified resource. * - * @param int $id + * @param integer $id * @return mixed * @throws \Illuminate\Auth\Access\AuthorizationException */ @@ -31,27 +32,35 @@ public function show(int $id) if (!$survey) { return response()->json( [ - 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], ], 404 ); } + return new SurveyResource($survey); - } + }//end show() + /** * Display the specified resource. + * * @return SurveyCollection * @throws \Illuminate\Auth\Access\AuthorizationException */ public function index() { return new SurveyCollection(Survey::all()); - } + }//end index() + /** * Display the specified resource. - * @TODO transactions =) + * + * @TODO transactions =) * @param Request $request * @return SurveyResource * @throws \Illuminate\Auth\Access\AuthorizationException @@ -68,11 +77,15 @@ public function store(Request $request) if ($user) { $this->authorize('store', Survey::class); } + $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey = Survey::create( array_merge( $request->input(), - [ 'updated' => time(), 'created' => time()] + [ + 'updated' => time(), + 'created' => time(), + ] ) ); $this->saveTranslations($request->input('translations'), $survey->id, 'survey'); @@ -81,86 +94,71 @@ public function store(Request $request) $stage_model = $survey->tasks()->create( array_merge( $stage, - [ 'updated' => time(), 'created' => time()] + [ + 'updated' => time(), + 'created' => time(), + ] ) ); - $this->saveTranslations($stage['translations'] ?? [], $stage_model->id, 'task'); + $this->saveTranslations(($stage['translations'] ?? []), $stage_model->id, 'task'); foreach ($stage['fields'] as $attribute) { $uuid = Uuid::uuid4(); $attribute['key'] = $uuid->toString(); $field_model = $stage_model->fields()->create( array_merge( $attribute, - [ 'updated' => time(), 'created' => time()] + [ + 'updated' => time(), + 'created' => time(), + ] ) ); - $this->saveTranslations($attribute['translations'] ?? [], $field_model->id, 'field'); + $this->saveTranslations(($attribute['translations'] ?? []), $field_model->id, 'field'); } - } - } + }//end foreach + }//end if + return new SurveyResource($survey); - } + }//end store() + /** - * @param $input - * @param $translatable_id - * @param $type - * @return bool + * @param $input + * @param $translatable_id + * @param $type + * @return boolean */ private function saveTranslations($input, int $translatable_id, string $type) { if (!is_array($input)) { return true; } - foreach ($input as $language => $translations) { - foreach ($translations as $key => $translated) { - if (is_array($translated)) { - $translated = json_encode($translated); - } - Translation::create([ - 'translatable_type' => $type, - 'translatable_id' => $translatable_id, - 'translated_key' => $key, - 'translation' => $translated, - 'language' => $language - ]); - } - } - } - /** - * @param $input - * @param $translatable_id - * @param $type - * @return bool - */ - private function updateTranslations($input, int $translatable_id, string $type) - { - if (!is_array($input)) { - return true; - } - Translation::where('translatable_id', $translatable_id) - ->where('translatable_type', $type) - ->delete(); foreach ($input as $language => $translations) { foreach ($translations as $key => $translated) { if (is_array($translated)) { $translated = json_encode($translated); } - Translation::create([ - 'translatable_type' => $type, - 'translatable_id' => $translatable_id, - 'translated_key' => $key, - 'translation' => $translated, - 'language' => $language - ]); + + Translation::create( + [ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language, + ] + ); } } - } + }//end saveTranslations() + + /** * Display the specified resource. - * @TODO transactions =) - * @param int $id + * + * @TODO transactions =) + * @param integer $id * @param Request $request * @return mixed * @throws \Illuminate\Auth\Access\AuthorizationException @@ -171,31 +169,73 @@ public function update(int $id, Request $request) if (!$survey) { return response()->json( [ - 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], ], 404 ); } + $this->authorize('update', $survey); if (!$survey) { return response()->json( [ - 'errors' => [ 'error' => 404, 'message' => 'Not found' ] + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], ], 404 ); } + $this->validate($request, Survey::getRules(), Survey::validationMessages()); $survey->update( array_merge( $request->input(), - [ 'updated' => time()] + ['updated' => time()] ) ); $this->updateTranslations($request->input('translations'), $survey->id, 'survey'); - $this->updateTasks($request->input('tasks') ?? [], $survey); + $this->updateTasks(($request->input('tasks') ?? []), $survey); return new SurveyResource($survey); - } + }//end update() + + + /** + * @param $input + * @param $translatable_id + * @param $type + * @return boolean + */ + private function updateTranslations($input, int $translatable_id, string $type) + { + if (!is_array($input)) { + return true; + } + + Translation::where('translatable_id', $translatable_id)->where('translatable_type', $type)->delete(); + foreach ($input as $language => $translations) { + foreach ($translations as $key => $translated) { + if (is_array($translated)) { + $translated = json_encode($translated); + } + + Translation::create( + [ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language, + ] + ); + } + } + }//end updateTranslations() + /** * @param array $input_tasks @@ -210,18 +250,23 @@ private function updateTasks(array $input_tasks, Survey $survey) if (!$stage_model) { continue; } + $stage_model->update($stage); $stage_model = Stage::find($stage['id']); } else { - $stage_model = $survey->tasks()->create(array_merge( - $stage, - [ 'updated' => time()] - )); + $stage_model = $survey->tasks()->create( + array_merge( + $stage, + ['updated' => time()] + ) + ); $added_tasks[] = $stage_model->id; } - $this->updateTranslations($stage['translations'] ?? [], $stage_model->id, 'task'); - $this->updateFields($stage['fields'] ?? [], $stage_model); - } + + $this->updateTranslations(($stage['translations'] ?? []), $stage_model->id, 'task'); + $this->updateFields(($stage['fields'] ?? []), $stage_model); + }//end foreach + $input_tasks_collection = new Collection($input_tasks); $survey->load('tasks'); @@ -232,7 +277,8 @@ private function updateTasks(array $input_tasks, Survey $survey) foreach ($tasks_to_delete as $task_to_delete) { Stage::where('id', $task_to_delete->id)->delete(); } - } + }//end updateTasks() + /** * @param array $input_fields @@ -248,18 +294,26 @@ private function updateFields(array $input_fields, Stage $stage) if (!$field_model) { continue; } + $field_model->update($field); $field_model = Attribute::find($field['id']); } else { $uuid = Uuid::uuid4(); - $field_model = $stage->fields()->create(array_merge( - $field, - [ 'updated' => time(), 'key' => $uuid->toString()] - )); + $field_model = $stage->fields()->create( + array_merge( + $field, + [ + 'updated' => time(), + 'key' => $uuid->toString(), + ] + ) + ); $added_fields[] = $field_model->id; - } - $this->updateTranslations($field['translations'] ?? [], $field_model->id, 'field'); - } + }//end if + + $this->updateTranslations(($field['translations'] ?? []), $field_model->id, 'field'); + }//end foreach + $input_fields_collection = new Collection($input_fields); $stage->load('fields'); @@ -270,14 +324,33 @@ private function updateFields(array $input_fields, Stage $stage) foreach ($fields_to_delete as $field_to_delete) { Attribute::where('id', $field_to_delete->id)->delete(); } - } + }//end updateFields() + + /** - * @param int $id + * @param integer $id */ public function delete(int $id, Request $request) { $survey = Survey::find($id); $this->authorize('delete', $survey); + $task_ids = $survey->tasks->modelKeys(); + + $field_ids = $survey->tasks->map(function ($task, $key) use (&$field_ids) { + return $task->fields->modelKeys(); + })->flatten(); + + Translation::whereIn('translatable_id', $task_ids) + ->where('translatable_type', 'task') + ->delete(); + + Translation::whereIn('translatable_id', $field_ids) + ->where('translatable_type', 'field') + ->delete(); + + $survey->translations()->delete(); + $survey->delete(); + return response()->json(['result' => ['deleted' => $id]]); - } -} + }//end delete() +}//end class From 7c78798923a24f4c0d4cce44a244145ba9f18503 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 12 May 2020 11:58:27 -0300 Subject: [PATCH 29/39] Fix validation messages with param2 --- v4/Models/Survey.php | 65 ++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 28bd802062..1f3543d021 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -42,7 +42,10 @@ class Survey extends Model * OR a policy authorizer which is a more or less accepted method to do it * (which uses the same $hidden type thing but it's much nicer obviously) */ - protected $hidden = ['description', 'icon']; + protected $hidden = [ + 'description', + 'icon', + ]; /** * The attributes that should be mutated to dates. @@ -125,11 +128,17 @@ public static function validationMessages() ), 'name.min' => trans( 'validation.min_length', - ['param2' => 2] + [ + 'param2' => 2, + 'field' => trans('fields.name'), + ] ), 'name.max' => trans( 'validation.max_length', - ['param2' => 255] + [ + 'param2' => 255, + 'field' => trans('fields.name'), + ] ), 'name.regex' => trans( 'validation.regex', @@ -187,7 +196,10 @@ public static function validationMessages() ), 'tasks.*.fields.*.label.max' => trans( 'validation.max_length', - ['param2' => trans('fields.tasks.fields.label')] + [ + 'param2' => 150, + 'field' => trans('fields.tasks.fields.label'), + ] ), 'tasks.*.fields.*.key.alpha_dash' => trans( 'validation.alpha_dash', @@ -195,7 +207,10 @@ public static function validationMessages() ), 'tasks.*.fields.*.key.max' => trans( 'validation.max_length', - ['param2' => trans('fields.tasks.fields.key')] + [ + 'param2' => 150, + 'field' => trans('fields.tasks.fields.key'), + ] ), 'tasks.*.fields.*.input.required' => trans( 'validation.not_empty', @@ -207,19 +222,19 @@ public static function validationMessages() ), 'tasks.*.fields.*.type.required' => trans( 'validation.not_empty', - ['param2' => trans('fields.tasks.fields.type')] + ['field' => trans('fields.tasks.fields.type')] ), 'tasks.*.fields.*.type.in' => trans( 'validation.in_array', - ['param2' => trans('fields.tasks.fields.type')] + ['field' => trans('fields.tasks.fields.type')] ), 'tasks.*.fields.*.priority.numeric' => trans( 'validation.numeric', - ['param2' => trans('fields.tasks.fields.priority')] + ['field' => trans('fields.tasks.fields.priority')] ), 'tasks.*.fields.*.cardinality.numeric' => trans( 'validation.numeric', - ['param2' => trans('fields.tasks.fields.cardinality')] + ['field' => trans('fields.tasks.fields.cardinality')] ), 'tasks.*.fields.*.response_private.boolean' => trans( 'validation.regex', @@ -230,6 +245,7 @@ public static function validationMessages() // [[$this, 'canMakePrivate'], [':value', $type]] // ] ]; + }//end validationMessages() @@ -245,7 +261,7 @@ protected static function getRules() 'required', 'min:2', 'max:255', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT, + 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, ], 'description' => [ 'string', @@ -265,16 +281,14 @@ protected static function getRules() 'targeted_survey' => ['boolean'], 'tasks.*.label' => [ 'required', - 'regex:' . LegacyValidator::REGEX_STANDARD_TEXT, - ], - 'tasks.*.type' => [ - Rule::in( - [ - 'post', - 'task', - ] - ) + 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, ], + 'tasks.*.type' => [Rule::in( + [ + 'post', + 'task', + ] + )], 'tasks.*.priority' => ['numeric'], 'tasks.*.icon' => ['alpha'], 'tasks.*.fields.*.label' => [ @@ -345,6 +359,7 @@ protected static function getRules() // should be removing that arbitrary limit since it's pretty rare // for it to be needed ]; + }//end getRules() @@ -357,6 +372,7 @@ public function getCanCreateAttribute() { $can_create = $this->getCanCreateRoles($this->id); return $can_create['roles']; + }//end getCanCreateAttribute() @@ -370,6 +386,7 @@ private function getCanCreateRoles($form_id) */ $form_repo = service('repository.form'); return $form_repo->getRolesThatCanCreatePosts($form_id); + }//end getCanCreateRoles() @@ -381,7 +398,7 @@ private function getCanCreateRoles($form_id) public function tasks() { $authorizer = service('authorizer.form'); - $user = $authorizer->getUser(); + $user = $authorizer->getUser(); // NOTE: this acl->hasPermission check is all `canUserEditForm` does, so we're doing that directly // to avoid an hydration issue with InteractsWithFormPermissions if ($authorizer->acl->hasPermission($user, Permission::MANAGE_POSTS)) { @@ -392,9 +409,8 @@ public function tasks() return $this->hasMany( 'v4\Models\Stage', 'form_id' - ) - ->where('form_stages.show_when_published', '=', '1') - ->where('form_stages.task_is_internal_only', '=', '0'); + )->where('form_stages.show_when_published', '=', '1')->where('form_stages.task_is_internal_only', '=', '0'); + }//end tasks() @@ -404,5 +420,8 @@ public function tasks() public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); + }//end translations() + + }//end class From 476ebb7bbba514418dd96ff0ad396dac35559ecd Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 13 May 2020 22:38:39 -0300 Subject: [PATCH 30/39] Add Index and Show actions for categories, add some category tests --- tests/integration/v4/acl.v4.feature | 53 +++ tests/integration/v4/forms/forms.v4.feature | 162 +------- tests/integration/v4/tags.v4.feature | 409 +++++++++++++++++++ tests/integration/v4/translations.v4.feature | 136 +++--- v4/Http/Controllers/CategoryController.php | 210 ++++++++++ v4/Http/Controllers/SurveyController.php | 2 - v4/Http/Resources/CategoryCollection.php | 26 ++ v4/Http/Resources/CategoryResource.php | 32 ++ v4/Http/Resources/TranslationResource.php | 1 - v4/Models/Category.php | 227 ++++++++++ v4/Models/Survey.php | 14 +- v4/Providers/MorphServiceProvider.php | 1 + v4/routes/web.php | 9 + 13 files changed, 1048 insertions(+), 234 deletions(-) create mode 100644 tests/integration/v4/tags.v4.feature create mode 100644 v4/Http/Controllers/CategoryController.php create mode 100644 v4/Http/Resources/CategoryCollection.php create mode 100644 v4/Http/Resources/CategoryResource.php create mode 100644 v4/Models/Category.php diff --git a/tests/integration/v4/acl.v4.feature b/tests/integration/v4/acl.v4.feature index 857cc28ff0..3e4680bb0b 100644 --- a/tests/integration/v4/acl.v4.feature +++ b/tests/integration/v4/acl.v4.feature @@ -163,3 +163,56 @@ Feature: V4 API Access Control Layer """ When I request "/surveys" Then the guzzle status code should be 403 + @rolesEnabled + Scenario: Import-Export user CANNOT create a hydrated form + Given that I want to make a new "Survey" + And that the oauth token is "testimporter" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "enabled_languages": { + "default": "en-EN" + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + "tasks": [ + { + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "fields": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {} + }, + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {} + } + ], + "is_public": true + } + ], + "name": "new" + } + """ + When I request "/surveys" + Then the guzzle status code should be 403 diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index b47802b6fc..49dd1f4497 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -1,8 +1,8 @@ -@oauth2Skip +@rolesEnabled Feature: Testing the Surveys API - Scenario: Creating a new Survey Given that I want to make a new "Survey" + And that the oauth token is "testmanager" And that the api_url is "api/v4" And that the request "data" is: """ @@ -29,6 +29,7 @@ Feature: Testing the Surveys API Scenario: Updating a Survey Given that I want to update a "Survey" + And that the oauth token is "testmanager" And that the api_url is "api/v4" And that the request "data" is: """ @@ -537,6 +538,7 @@ Feature: Testing the Surveys API Scenario: Updating a Survey to clear name should fail Given that I want to update a "Survey" And that the api_url is "api/v4" + And that the oauth token is "testmanager" And that the request "data" is: """ { @@ -556,6 +558,7 @@ Feature: Testing the Surveys API Scenario: Update a non-existent Survey Given that I want to update a "Survey" And that the api_url is "api/v4" + And that the oauth token is "testmanager" And that the request "data" is: """ { @@ -574,6 +577,7 @@ Feature: Testing the Surveys API Scenario: Listing All Surveys Given that I want to get all "Surveys" And that the api_url is "api/v4" + And that the oauth token is "testmanager" When I request "/surveys" Then the response is JSON And the "results" property count is "9" @@ -583,6 +587,7 @@ Feature: Testing the Surveys API Given that I want to find a "Survey" And that its "id" is "1" And that the api_url is "api/v4" + And that the oauth token is "testmanager" When I request "/surveys" Then the response is JSON And the response has a "result.id" property @@ -593,6 +598,7 @@ Feature: Testing the Surveys API Given that I want to delete a "Survey" And that its "id" is "1" And that the api_url is "api/v4" + And that the oauth token is "testmanager" When I request "/surveys" Then the response is JSON And the response has a "result.deleted" property @@ -602,159 +608,9 @@ Feature: Testing the Surveys API Scenario: Finding a non-existent Survey Given that I want to find a "Survey" And that the api_url is "api/v4" + And that the oauth token is "testmanager" And that its "id" is "1" When I request "/surveys" Then the response is JSON And the response has a "errors" property Then the guzzle status code should be 404 -## -## Scenario: POST method disabled for Survey Roles -## Given that I want to make a new "SurveyRole" -## And that the api_url is "api/v4" -## And that the request "data" is: -## """ -## { -## "roles": [1] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "errors" property -## Then the guzzle status code should be 405 -## -## Scenario: DELETE method disabled for Survey Roles -## Given that I want to delete a "SurveyRole" -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "errors" property -## Then the guzzle status code should be 405 -## -## Scenario: Add 1 role to Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [1] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "count" property -## And the "count" property equals "1" -## Then the guzzle status code should be 200 -## -## Scenario: Add 2 roles to Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [1,2] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "count" property -## And the "count" property equals "2" -## Then the guzzle status code should be 200 -## -## Scenario: Finding a Survey after roles have been set. -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [1,2] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## Given that I want to find a "Survey" -## And that its "id" is "1" -## When I request "/surveys" -## Then the response is JSON -## And the response has a "id" property -## And the type of the "id" property is "numeric" -## And the response has a "can_create" property -## And the response has a "can_create.0" property -## And the "can_create.0" property equals "user" -## And the response has a "can_create.1" property -## And the "can_create.1" property equals "admin" -## Then the guzzle status code should be 200 -## -## Scenario: Remove roles from Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "count" property -## And the "count" property equals "0" -## Then the guzzle status code should be 200 -## -## Scenario: Finding all Survey Roles -## Given that I want to find a "SurveyRole" -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "count" property -## And the type of the "count" property is "2" -## Then the guzzle status code should be 200 -## -## Scenario: Fail to add 1 invalid Role to Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [120] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "errors" property -## Then the guzzle status code should be 422 -## -## Scenario: Fail to add roles with 1 invalid Role id to Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [1,2,120] -## } -## """ -## When I request "/surveys/1/roles" -## Then the response is JSON -## And the response has a "errors" property -## Then the guzzle status code should be 422 -## -## Scenario: Add roles to non-existent Survey -## Given that I want to update a "SurveyRole" -## And that the request "data" is: -## """ -## { -## "roles": [1] -## } -## """ -## When I request "/surveys/26/roles" -## Then the response is JSON -## And the response has a "errors" property -## Then the guzzle status code should be 404 -# -# Scenario: Delete a Survey -# Given that I want to delete a "Survey" -# And that the api_url is "api/v4" -# And that its "id" is "1" -# When I request "/surveys" -# Then the response is JSON -# And the response has a "id" property -# Then the guzzle status code should be 200 -# -# Scenario: Fail to delete a non existent Survey -# Given that I want to delete a "Survey" -# And that the api_url is "api/v4" -# And that its "id" is "35" -# When I request "/surveys" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 404 diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature new file mode 100644 index 0000000000..47ef2bfc1e --- /dev/null +++ b/tests/integration/v4/tags.v4.feature @@ -0,0 +1,409 @@ +@tagsFixture @rolesEnabled +Feature: Testing the Categories API +# Scenario: Creating a new Tag +# Given that I want to make a new "Category" +# And that the api_url is "api/v4" +# And that the request "data" is: +# """ +# { +# "parent_id":1, +# "tag":"Boxes", +# "slug":"boxes", +# "description":"Is this a box? Awesome", +# "type":"category", +# "priority":1, +# "color":"00ff00", +# "role": ["admin", "user"] +# } +# """ +# When I request "/categories" +# Then the response is JSON +# And the response has a "result.id" property +# And the type of the "result.id" property is "numeric" +# And the "result.tag" property equals "Boxes" +# And the "result.slug" property equals "boxes" +# And the "result.description" property equals "Is this a box? Awesome" +# And the "result.color" property equals "#00ff00" +# And the "result.priority" property equals "1" +# And the "result.type" property equals "category" +# And the response has a "role" property +# And the "result.parent.id" property equals "1" +# Then the guzzle status code should be 200 +# +# Scenario: Creating a duplicate tag +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Duplicate", +# "type":"category", +# "priority":1 +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 422 +# +# Scenario: Creating a tag with a duplicate slug +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Something", +# "slug":"duplicate", +# "type":"category", +# "priority":1 +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 422 +# +# Scenario: Creating a tag with a long name fails +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long", +# "type":"category" +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 422 +# +# Scenario: Check slug is generated on new tag +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"My magical tag", +# "type":"category", +# "priority":1 +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the type of the "id" property is "numeric" +# And the response has a "slug" property +# And the "slug" property equals "my-magical-tag" +# Then the guzzle status code should be 200 +# +# Scenario: Check hash on color input has no effect when creating tag +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Another tag", +# "type":"category", +# "priority":1, +# "color":"#00ff00" +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the type of the "id" property is "numeric" +# And the "color" property equals "#00ff00" +# Then the guzzle status code should be 200 +# +# Scenario: Creating a tag with non-existent parent fails +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Superduper tag", +# "type":"category", +# "priority":1, +# "parent_id":10001 +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 422 +# +# Scenario: Updating a Tag +# Given that I want to update a "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Updated", +# "slug":"updated", +# "type":"status", +# "priority":1 +# } +# """ +# And that its "id" is "1" +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the type of the "id" property is "numeric" +# And the "id" property equals "1" +# And the response has a "tag" property +# And the "tag" property equals "Updated" +# Then the guzzle status code should be 200 +# +# Scenario: Updating a non-existent Tag +# Given that I want to update a "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Updated", +# "slug":"updated", +# "type":"varchar", +# "priority":1 +# } +# """ +# And that its "id" is "40" +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 404 +# +# @resetFixture +# Scenario: Updating Tag Role Restrictions +# Given that I want to update a "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Change Role", +# "slug":"change-role", +# "type":"status", +# "role":["user"] +# } +# """ +# And that its "id" is "1" +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the "id" property equals "1" +# And the response has a "role" property +# And the "role.0" property equals "user" +# Then the guzzle status code should be 200 +# +# Scenario: Removing Tag Role Restrictions +# Given that I want to update a "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Change Role", +# "slug":"change-role", +# "type":"status", +# "role":[] +# } +# """ +# And that its "id" is "1" +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the "id" property equals "1" +# And the response has a "role" property +# And the "role" property is empty +# Then the guzzle status code should be 200 +# + @resetFixture + Scenario: Listing All Tags + Given that I want to get all "Tags" + When I request "/categories" + Then the response is JSON + And the "results" property count is "11" + Then the guzzle status code should be 200 + +# @resetFixture +# Scenario: Search All Tags +# Given that I want to get all "Tags" +# And that the request "query string" is: +# """ +# q=Explo +# """ +# When I request "/tags" +# Then the response is JSON +# And the "count" property equals "1" +# And the "results.0.tag" property equals "Explosion" +# Then the guzzle status code should be 200 +# +# @resetFixture +# Scenario: Search All Tags by type +# Given that I want to get all "Tags" +# And that the request "query string" is: +# """ +# type=category +# """ +# When I request "/tags" +# Then the response is JSON +# And the "count" property equals "9" +# Then the guzzle status code should be 200 +# +# @resetFixture +# Scenario: Search All Tags by parent +# Given that I want to get all "Tags" +# And that the request "query string" is: +# """ +# parent_id=3 +# """ +# When I request "/tags" +# Then the response is JSON +# And the "count" property equals "1" +# Then the guzzle status code should be 200 +# +# Scenario: Finding a Tag +# Given that I want to find a "Tag" +# And that its "id" is "1" +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the type of the "id" property is "numeric" +# Then the guzzle status code should be 200 +# +# Scenario: Finding a non-existent Tag +# Given that I want to find a "Tag" +# And that its "id" is "35" +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# Then the guzzle status code should be 404 +# +# Scenario: Deleting a Tag +# Given that I want to delete a "Tag" +# And that its "id" is "1" +# When I request "/tags" +# Then the guzzle status code should be 200 +# +# @resetFixture +# Scenario: Deleting a tag removes it from attribute options +# Given that I want to delete a "Tag" +# And that its "id" is "1" +# When I request "/tags" +# Then the guzzle status code should be 200 +# Given that I want to find a "Attribute" +# And that its "id" is "26" +# When I request "/forms/1/attributes" +# Then the response is JSON +# And the response has an "options" property +# And the "options" property does not contain "1" +# Then the guzzle status code should be 200 +# +# Scenario: Deleting a non-existent Tag +# Given that I want to delete a "Tag" +# And that its "id" is "35" +# When I request "/tags" +# And the response has a "errors" property +# Then the guzzle status code should be 404 +# +# Scenario: Creating a new child for a tag with role=admin +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "parent_id":9, +# "tag":"Valid child", +# "slug":"valid-child", +# "description":"I am a valid tag", +# "type":"category", +# "priority":1, +# "color":"00ff00", +# "role": "admin" +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "id" property +# And the type of the "id" property is "numeric" +# And the "tag" property equals "Valid child" +# And the "slug" property equals "valid-child" +# And the "description" property equals "I am a valid tag" +# And the "color" property equals "#00ff00" +# And the "priority" property equals "1" +# And the "type" property equals "category" +# And the response has a "role" property +# And the type of the "role" property is "array" +# And the "parent.id" property equals "9" +# Then the guzzle status code should be 200 +# +# Scenario: Creating a new invalid child for a tag with role=admin +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "parent_id":9, +# "tag":"Not a valid tag role", +# "slug":"not-valid-tag-role", +# "description":"My role is invalid", +# "type":"category", +# "priority":1, +# "color":"00ff00", +# "role":"nope" +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# And the "errors.1.message" property contains "Role nope does not exist" +# Then the guzzle status code should be 422 +# +# Scenario: Creating a new child with no role for a tag with role=admin +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "parent_id":9, +# "tag":"Not a valid tag role", +# "slug":"not-valid-tag-role", +# "description":"My role is invalid", +# "type":"category", +# "priority":1, +# "color":"00ff00", +# "role":null +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# And the "errors.1.message" property contains "Role must match the parent category" +# Then the guzzle status code should be 422 +# +# @resetFixture +# Scenario: Creating a new invalid child for a tag with role=admin +# Given that I want to make a new "Tag" +# And that the request "data" is: +# """ +# { +# "parent_id":9, +# "tag":"Not a valid tag role", +# "slug":"also-not-valid-tag-role", +# "description":"My role is invalid", +# "type":"category", +# "priority":1, +# "color":"00ff00", +# "role":"user" +# } +# """ +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# And the "errors.1.message" property equals "Role must match the parent category" +# Then the guzzle status code should be 422 +# +# Scenario: Updating a child tag to a different role from its parent should fail +# Given that I want to update a "Tag" +# And that the request "data" is: +# """ +# { +# "tag":"Child 2", +# "slug":"child-2", +# "type":"category", +# "role":"user" +# } +# """ +# And that its "id" is "11" +# When I request "/tags" +# Then the response is JSON +# And the response has a "errors" property +# And the "errors.1.message" property equals "Role must match the parent category" +# Then the guzzle status code should be 422 +# diff --git a/tests/integration/v4/translations.v4.feature b/tests/integration/v4/translations.v4.feature index 41315120da..43523d81aa 100644 --- a/tests/integration/v4/translations.v4.feature +++ b/tests/integration/v4/translations.v4.feature @@ -1,83 +1,83 @@ @acl Feature: Testing translations @rolesEnabled - Scenario: User with Manage Settings permission can create a hydrated form + Scenario: User with Manage Settings permission can create a hydrated form Given that I want to make a new "Survey" And that the oauth token is "testmanager" And that the api_url is "api/v4" And that the request "data" is: """ - { - "enabled_languages": { - "default": "en-EN", - "available": ["es"] - }, - "color": null, - "require_approval": true, - "everyone_can_create": false, - "targeted_survey": false, - "translations": { - "es": { - "name": "nombre" - } - }, - "tasks": [ - { - "translations": { - "es": { - "label": "Reporte", - "description": "Una descripcion" + { + "enabled_languages": { + "default": "en-EN", + "available": ["es"] + }, + "color": null, + "require_approval": true, + "everyone_can_create": false, + "targeted_survey": false, + "translations": { + "es": { + "name": "nombre" + } + }, + "tasks": [ + { + "translations": { + "es": { + "label": "Reporte", + "description": "Una descripcion" + } + }, + "label": "Post", + "priority": 0, + "required": false, + "type": "post", + "show_when_published": true, + "task_is_internal_only": false, + "fields": [ + { + "cardinality": 0, + "input": "text", + "label": "Title", + "priority": 1, + "required": true, + "type": "title", + "options": [], + "config": {}, + "translations": { + "es": { + "label": "Titulo", + "instructions": "Instrucciones", + "default": "Un valor por defecto", + "options": ["Una opcion", "otra opcion"] + } } }, - "label": "Post", - "priority": 0, - "required": false, - "type": "post", - "show_when_published": true, - "task_is_internal_only": false, - "fields": [ - { - "cardinality": 0, - "input": "text", - "label": "Title", - "priority": 1, - "required": true, - "type": "title", - "options": [], - "config": {}, - "translations": { - "es": { - "label": "Titulo", - "instructions": "Instrucciones", - "default": "Un valor por defecto", - "options": ["Una opcion", "otra opcion"] - } - } - }, - { - "cardinality": 0, - "input": "text", - "label": "Description", - "priority": 2, - "required": true, - "type": "description", - "options": [], - "config": {}, - "translations": { - "es": { - "label": "Descripcion", - "instructions": "Instrucciones de la desc", - "default": "Un valor por defecto para desc", - "options": ["Una opcion", "otra opcion desc"] - } + { + "cardinality": 0, + "input": "text", + "label": "Description", + "priority": 2, + "required": true, + "type": "description", + "options": [], + "config": {}, + "translations": { + "es": { + "label": "Descripcion", + "instructions": "Instrucciones de la desc", + "default": "Un valor por defecto para desc", + "options": ["Una opcion", "otra opcion desc"] } } - ], - "is_public": true - } - ], - "name": "new" - } + } + ], + "is_public": true + } + ], + "name": "new" + } """ When I request "/surveys" Then the response is JSON diff --git a/v4/Http/Controllers/CategoryController.php b/v4/Http/Controllers/CategoryController.php new file mode 100644 index 0000000000..851b499aee --- /dev/null +++ b/v4/Http/Controllers/CategoryController.php @@ -0,0 +1,210 @@ +with('translations')->find($id); + if (!$category) { + return response()->json( + [ + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], + ], + 404 + ); + } + + return new CategoryResource($category); + }//end show() + + + /** + * Display the specified resource. + * + * @return CategoryCollection + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index() + { + return new CategoryCollection(Category::allowed()->get()); + }//end index() + + + /** + * Display the specified resource. + * + * @TODO transactions =) + * @param Request $request + * @return SurveyResource + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function store(Request $request) + { + $authorizer = service('authorizer.form'); + // if there's no user the guards will kick them off already, but if there + // is one we need to check the authorizer to ensure we don't let + // users without admin perms create forms etc + // this is an unfortunate problem with using an old version of lumen + // that doesn't let me do guest user checks without adding more risk. + $user = $authorizer->getUser(); + if ($user) { + $this->authorize('store', Category::class); + } + + $this->validate($request, Category::getRules(), Category::validationMessages()); + $category = Category::create( + array_merge( + $request->input(), + [ + 'created' => time(), + ] + ) + ); + $this->saveTranslations($request->input('translations'), $category->id, 'category'); + return new CategoryResource($category); + }//end store() + + + /** + * @param $input + * @param $translatable_id + * @param $type + * @return boolean + */ + private function saveTranslations($input, int $translatable_id, string $type) + { + if (!is_array($input)) { + return true; + } + + foreach ($input as $language => $translations) { + foreach ($translations as $key => $translated) { + if (is_array($translated)) { + $translated = json_encode($translated); + } + + Translation::create( + [ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language, + ] + ); + } + } + }//end saveTranslations() + + + /** + * Display the specified resource. + * + * @TODO transactions =) + * @param integer $id + * @param Request $request + * @return mixed + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function update(int $id, Request $request) + { + $category = Category::find($id); + if (!$category) { + return response()->json( + [ + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], + ], + 404 + ); + } + + $this->authorize('update', $category); + if (!$category) { + return response()->json( + [ + 'errors' => [ + 'error' => 404, + 'message' => 'Not found', + ], + ], + 404 + ); + } + + $this->validate($request, Category::getRules(), Category::validationMessages()); + $category->update($request->input()); + $this->updateTranslations($request->input('translations'), $category->id, 'category'); + return new CategoryResource($category); + }//end update() + + + /** + * @param $input + * @param $translatable_id + * @param $type + * @return boolean + */ + private function updateTranslations($input, int $translatable_id, string $type) + { + if (!is_array($input)) { + return true; + } + + Translation::where('translatable_id', $translatable_id)->where('translatable_type', $type)->delete(); + foreach ($input as $language => $translations) { + foreach ($translations as $key => $translated) { + if (is_array($translated)) { + $translated = json_encode($translated); + } + + Translation::create( + [ + 'translatable_type' => $type, + 'translatable_id' => $translatable_id, + 'translated_key' => $key, + 'translation' => $translated, + 'language' => $language, + ] + ); + } + } + }//end updateTranslations() + + + /** + * @param integer $id + */ + public function delete(int $id, Request $request) + { + $category = Category::find($id); + $this->authorize('delete', $category); + $category->translations()->delete(); + $category->delete(); + return response()->json(['result' => ['deleted' => $id]]); + }//end delete() +}//end class diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 0ac7160559..488ed0d363 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -17,8 +17,6 @@ class SurveyController extends V4Controller { - - /** * Display the specified resource. * diff --git a/v4/Http/Resources/CategoryCollection.php b/v4/Http/Resources/CategoryCollection.php new file mode 100644 index 0000000000..f7f5fec3b6 --- /dev/null +++ b/v4/Http/Resources/CategoryCollection.php @@ -0,0 +1,26 @@ +collection; + } +} diff --git a/v4/Http/Resources/CategoryResource.php b/v4/Http/Resources/CategoryResource.php new file mode 100644 index 0000000000..21ce7cee74 --- /dev/null +++ b/v4/Http/Resources/CategoryResource.php @@ -0,0 +1,32 @@ + $this->id, + 'parent_id' => $this->parent_id, + 'tag' => $this->tag, + 'slug' => $this->slug, + 'type' => $this->type, + 'color' => $this->color, + 'icon' => $this->icon, + 'description' => $this->description, + 'role' => $this->role, + 'priority' => $this->priority, + 'translations' => new TranslationCollection($this->translations), + ]; + } +} diff --git a/v4/Http/Resources/TranslationResource.php b/v4/Http/Resources/TranslationResource.php index 7375bcb227..7f04291c8e 100644 --- a/v4/Http/Resources/TranslationResource.php +++ b/v4/Http/Resources/TranslationResource.php @@ -19,7 +19,6 @@ public function toArray($request) 'key' => $this->translated_key, 'translation' => $this->translation, 'language' => $this->language - ]; } } diff --git a/v4/Models/Category.php b/v4/Models/Category.php new file mode 100644 index 0000000000..233ee48ce2 --- /dev/null +++ b/v4/Models/Category.php @@ -0,0 +1,227 @@ + 'category' + ]; + + protected $casts = [ + 'role' => 'json' + ]; + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public static function validationMessages() + { + return [ + 'tag.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.tag')] + ), + 'tag.min' => trans( + 'validation.min_length', + [ + 'param2' => 2, + 'field' => trans('fields.tag'), + ] + ), + 'tag.max' => trans( + 'validation.max_length', + [ + 'param2' => 255, + 'field' => trans('fields.tag'), + ] + ), + 'tag.regex' => trans( + 'validation.regex', + ['field' => trans('fields.tag')] + ), + 'slug.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.slug')] + ), + 'slug.min' => trans( + 'validation.min_length', + [ + 'param2' => 2, + 'field' => trans('fields.slug'), + ] + ), + 'type.required' => trans( + 'validation.not_empty', + ['field' => trans('fields.type')] + ), + 'type.in' => trans( + 'validation.in_array', + ['field' => trans('fields.type')] + ), + 'description.regex' => trans( + 'validation.regex', + ['field' => trans('fields.description')] + ), + 'icon.regex' => trans( + 'validation.regex', + ['field' => trans('fields.icon')] + ), + 'priority.numeric' => trans( + 'validation.numeric', + ['field' => trans('fields.priority')] + ), + ]; + }//end translations() + + /** + * Return all validation rules + * @return array + */ + protected static function getRules() + { + return [ + // 'parent_id' = [ + // [[$this->repo, 'doesTagExist'], [':value']], + // ] + 'tag' => [ + 'required', + 'min:2', + 'max:255', + 'regex:/^[\pL\pN\pP ]++$/uD' + ], + 'slug' => [ + 'required', + 'min:2', + // [[$this->repo, 'isSlugAvailable'], [':value']], + // 'min:2', + // 'max:255', + // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, + ], + 'type' => [ + 'required', + Rule::in([ + 'category', + 'status' + ]) + // 'min:2', + // 'max:255', + // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, + ], + 'description' => [ + 'regex:/^[\pL\pN\pP ]++$/uD' + // 'min:2', + // 'max:255', + // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, + ], + // 'color' => [ + // ['color'], + // ] + 'icon' => [ + 'regex:/^[\pL\s\_\-]++$/uD' + ], + 'priority' => [ + 'numeric' + ], + // 'role' => [ + // [[$this->role_repo, 'exists'], [':value']], + // [[$this->repo, 'isRoleValid'], [':validation', ':fulldata']] + // ] + ]; + }//end validationMessages() + + /** + * Get the category's translation. + */ + public function translations() + { + return $this->morphMany('v4\Models\Translation', 'translatable'); + }//end getRules() + + /** + * Scope helper to only pull tags we are allowed to get from the db + * @param $query + * @return mixed + */ + public function scopeAllowed($query) + { + /** + * If no roles are selected, the Tag is considered + * completely public. + */ + $authorizer = service('authorizer.tag'); + $user = $authorizer->getUser(); + + if ($user->role) { + // couldn't think of a better way to deal with our JSON-but-not-json fields + return $query->where(function ($query) use ($user) { + return $query + ->whereNull('role') + ->orWhere('role', 'LIKE', '%\"' . $user->role . '\"%'); + }); + } + return $query->whereNull('role'); + } +}//end class diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 1f3543d021..9568139cde 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -218,7 +218,7 @@ public static function validationMessages() ), 'tasks.*.fields.*.input.in' => trans( 'validation.in_array', - ['param2' => trans('fields.tasks.fields.input')] + ['field' => trans('fields.tasks.fields.input')] ), 'tasks.*.fields.*.type.required' => trans( 'validation.not_empty', @@ -245,7 +245,6 @@ public static function validationMessages() // [[$this, 'canMakePrivate'], [':value', $type]] // ] ]; - }//end validationMessages() @@ -359,7 +358,6 @@ protected static function getRules() // should be removing that arbitrary limit since it's pretty rare // for it to be needed ]; - }//end getRules() @@ -372,7 +370,6 @@ public function getCanCreateAttribute() { $can_create = $this->getCanCreateRoles($this->id); return $can_create['roles']; - }//end getCanCreateAttribute() @@ -386,7 +383,6 @@ private function getCanCreateRoles($form_id) */ $form_repo = service('repository.form'); return $form_repo->getRolesThatCanCreatePosts($form_id); - }//end getCanCreateRoles() @@ -409,8 +405,9 @@ public function tasks() return $this->hasMany( 'v4\Models\Stage', 'form_id' - )->where('form_stages.show_when_published', '=', '1')->where('form_stages.task_is_internal_only', '=', '0'); - + ) + ->where('form_stages.show_when_published', '=', '1') + ->where('form_stages.task_is_internal_only', '=', '0'); }//end tasks() @@ -420,8 +417,5 @@ public function tasks() public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); - }//end translations() - - }//end class diff --git a/v4/Providers/MorphServiceProvider.php b/v4/Providers/MorphServiceProvider.php index 231b6b3b51..ba6a98418a 100644 --- a/v4/Providers/MorphServiceProvider.php +++ b/v4/Providers/MorphServiceProvider.php @@ -14,6 +14,7 @@ public function boot() 'survey' => 'v4\Models\Survey', 'task' => 'v4\Models\Stage', 'field' => 'v4\Models\Attribute', + 'category' => 'v4\Models\Category' ]); } } diff --git a/v4/routes/web.php b/v4/routes/web.php index c99b977032..d83b79989d 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -19,6 +19,15 @@ $router->get('/{id}', 'SurveyController@show'); }); + $router->group([ + 'prefix' => 'categories', + 'middleware' => ['scope:tags', 'expiration'] + ], function () use ($router) { + // Public access + $router->get('/', 'CategoryController@index'); + $router->get('/{id}', 'CategoryController@show'); + }); + // Restricted access $router->group([ 'prefix' => 'surveys', From 9f4267a27f5cb56f64a926c6ae69f4cbcb3f9d16 Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 13 May 2020 22:40:03 -0300 Subject: [PATCH 31/39] Add Index and Show actions for categories, add some category tests --- tests/integration/v4/tags.v4.feature | 2 +- v4/Http/Resources/CategoryCollection.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature index 47ef2bfc1e..0d530b06cc 100644 --- a/tests/integration/v4/tags.v4.feature +++ b/tests/integration/v4/tags.v4.feature @@ -208,7 +208,7 @@ Feature: Testing the Categories API # @resetFixture Scenario: Listing All Tags - Given that I want to get all "Tags" + Given that I want to get all "Categories" When I request "/categories" Then the response is JSON And the "results" property count is "11" diff --git a/v4/Http/Resources/CategoryCollection.php b/v4/Http/Resources/CategoryCollection.php index f7f5fec3b6..7fd3a6fd18 100644 --- a/v4/Http/Resources/CategoryCollection.php +++ b/v4/Http/Resources/CategoryCollection.php @@ -7,6 +7,8 @@ class CategoryCollection extends ResourceCollection { + public static $wrap = 'results'; + /** * The resource that this resource collects. * From 515616adbc739621c181052a067187f603560b8f Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 13 May 2020 22:45:41 -0300 Subject: [PATCH 32/39] Add Index and Show actions for categories, add some category tests - add and fix some more tests --- tests/integration/v4/tags.v4.feature | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature index 0d530b06cc..5564a42572 100644 --- a/tests/integration/v4/tags.v4.feature +++ b/tests/integration/v4/tags.v4.feature @@ -207,11 +207,28 @@ Feature: Testing the Categories API # Then the guzzle status code should be 200 # @resetFixture - Scenario: Listing All Tags + Scenario: Listing All Tags available to admins Given that I want to get all "Categories" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" When I request "/categories" Then the response is JSON - And the "results" property count is "11" + And the "results" property count is "7" + Then the guzzle status code should be 200 + Scenario: Listing All Tags available to regular users + Given that I want to get all "Categories" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + When I request "/categories" + Then the response is JSON + And the "results" property count is "6" + Then the guzzle status code should be 200 + Scenario: Listing All Tags available to non-users + Given that I want to get all "Categories" + And that the api_url is "api/v4" + When I request "/categories" + Then the response is JSON + And the "results" property count is "5" Then the guzzle status code should be 200 # @resetFixture From 8da3705285f4d60c50602ae8457570b18f8cf78b Mon Sep 17 00:00:00 2001 From: Romina Date: Thu, 14 May 2020 00:12:08 -0300 Subject: [PATCH 33/39] Add correct permission restrictions to categories for child-and-parents --- app/Providers/AuthServiceProvider.php | 1 + v4/Models/Category.php | 51 +++++++++- v4/Policies/CategoryPolicy.php | 137 ++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 v4/Policies/CategoryPolicy.php diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 93674d7117..8a21472ae2 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -61,6 +61,7 @@ public function boot() $this->defineScopes(); // need to use a string here or laravel goes wild and doesn't authorize anything Gate::policy('v4\Models\Survey', 'v4\Policies\SurveyPolicy'); + Gate::policy('v4\Models\Category', 'v4\Policies\CategoryPolicy'); } protected function defineScopes() diff --git a/v4/Models/Category.php b/v4/Models/Category.php index 233ee48ce2..b104640bda 100644 --- a/v4/Models/Category.php +++ b/v4/Models/Category.php @@ -216,12 +216,57 @@ public function scopeAllowed($query) if ($user->role) { // couldn't think of a better way to deal with our JSON-but-not-json fields - return $query->where(function ($query) use ($user) { + // get categories that are available for users with this role or NULL role + // taking care NOT to bring any child categories that belong + // to parents with other role restrictions + $q = $query->where(function ($query) use ($user) { return $query ->whereNull('role') - ->orWhere('role', 'LIKE', '%\"' . $user->role . '\"%'); + ->orWhere('role', 'LIKE', '%\"' . $user->role . '\"%') + ; }); + $q->where(function ($query) use ($user) { + return $query + ->whereNotIn('parent_id', function ($query) use ($user) { + $query + ->select('id') + ->from('tags') + ->where('role', 'NOT LIKE', '%\"' . $user->role . '\"%') + ->whereNull('parent_id'); + }) + ->orWhereNull('parent_id'); + }); + // generates a query like like this + // select * from `tags` where (`role` is null or `role` LIKE ?) + // AND (`parent_id` not in + // ( + // select `id` from `tags` where `role` NOT LIKE ? and `parent_id` is null + // ) + // or `parent_id` is null) + return $q; } - return $query->whereNull('role'); + // get categories that are available for non logged in users + // taking care NOT to bring any child categories that belong + // to parents with admin/user/other role restrictions + $q = $query->whereNull('role')->where(function ($query) use ($user) { + return $query + ->whereNotIn('parent_id', function ($query) use ($user) { + $query + ->select('id') + ->from('tags') + ->whereNotNull('role') + ->whereNull('parent_id'); + }) + ->orWhereNull('parent_id'); + }); + // generates a query like this: + // select * from `tags` where `role` is null + // AND (`parent_id` not in + // ( + // select `id` from `tags` where `role` is not null and `parent_id` is null + // ) + // or `parent_id` is null) + + return $q; } }//end class diff --git a/v4/Policies/CategoryPolicy.php b/v4/Policies/CategoryPolicy.php new file mode 100644 index 0000000000..c6221e45ac --- /dev/null +++ b/v4/Policies/CategoryPolicy.php @@ -0,0 +1,137 @@ +isAllowed($empty_tag, 'search'); + } + + /** + * + * @param GenericUser $user + * @param Category $category + * @return bool + */ + public function show(User $user, Category $category) + { + $tag = new Entity\Tag($category->toArray()); + return $this->isAllowed($tag, 'read'); + } + + /** + * + * @param GenericUser $user + * @param Category $category + * @return bool + */ + public function delete(User $user, Category $category) + { + $tag = new Entity\Tag($category->toArray()); + return $this->isAllowed($tag, 'delete'); + } + /** + * @param Category $category + * @return bool + */ + public function update(User $user, Category $category) + { + // we convert to a form entity to be able to continue using the old authorizers and classes. + $tag = new Entity\Tag($category->toArray()); + return $this->isAllowed($tag, 'update'); + } + + + /** + * @param Survey $survey + * @return bool + */ + public function store() + { + // we convert to a form entity to be able to continue using the old authorizers and classes. + $tag = new Entity\Tag(); + return $this->isAllowed($tag, 'create'); + } + /** + * @param $entity + * @param string $privilege + * @return bool + */ + public function isAllowed($entity, $privilege) + { + $authorizer = service('authorizer.tag'); + + // These checks are run within the user context. + $user = $authorizer->getUser(); + + // Only logged in users have access if the deployment is private + if (!$this->canAccessDeployment($user)) { + return false; + } + + // First check whether there is a role with the right permissions + if ($this->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { + return true; + } + + // Then we check if a user has the 'admin' role. If they do they're + // allowed access to everything (all entities and all privileges) + if ($this->isUserAdmin($user)) { + return true; + } + + // isAllowParent is usually checked here in v3, but we do + // it at the eloquent level instead + + // isUserOfRole is usually checked here in v3, but we do + // it at the eloquent level instead + + if ($privilege === 'search') { + return true; + } + + // If no other access checks succeed, we default to denying access + return false; + } +} From 8eb21c1e339dca8fd50695bd1c5a8fb9a8fbed6d Mon Sep 17 00:00:00 2001 From: Romina Date: Thu, 14 May 2020 00:18:18 -0300 Subject: [PATCH 34/39] Add parent() and children() relation to itself in categories, erasing a lot of weird frontend logic needed --- v4/Http/Resources/CategoryResource.php | 2 ++ v4/Models/Category.php | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/v4/Http/Resources/CategoryResource.php b/v4/Http/Resources/CategoryResource.php index 21ce7cee74..c0ad46df7f 100644 --- a/v4/Http/Resources/CategoryResource.php +++ b/v4/Http/Resources/CategoryResource.php @@ -26,6 +26,8 @@ public function toArray($request) 'description' => $this->description, 'role' => $this->role, 'priority' => $this->priority, + 'children' => $this->children, + 'parent' => $this->parent, 'translations' => new TranslationCollection($this->translations), ]; } diff --git a/v4/Models/Category.php b/v4/Models/Category.php index b104640bda..e8389db916 100644 --- a/v4/Models/Category.php +++ b/v4/Models/Category.php @@ -156,9 +156,6 @@ protected static function getRules() 'required', 'min:2', // [[$this->repo, 'isSlugAvailable'], [':value']], - // 'min:2', - // 'max:255', - // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, ], 'type' => [ 'required', @@ -200,6 +197,15 @@ public function translations() return $this->morphMany('v4\Models\Translation', 'translatable'); }//end getRules() + public function parent() + { + return $this->hasOne('v4\Models\Category', 'id', 'parent_id'); + } + public function children() + { + return $this->hasMany('v4\Models\Category', 'parent_id', 'id'); + } + /** * Scope helper to only pull tags we are allowed to get from the db * @param $query From 39cf1de28e3f9c087574e2aadcad3a6d79fe051f Mon Sep 17 00:00:00 2001 From: Romina Date: Fri, 15 May 2020 01:17:41 -0300 Subject: [PATCH 35/39] Add color fix --- tests/integration/v4/forms/forms.v4.feature | 7 +++++-- v4/Http/Controllers/SurveyController.php | 1 - v4/Models/Survey.php | 23 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index 49dd1f4497..cd362b8dd5 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -10,7 +10,8 @@ Feature: Testing the Surveys API "name":"Test Survey", "type":"report", "description":"This is a test form from BDD testing", - "disabled":false + "disabled":false, + "color": "#A51A1A" } """ When I request "/surveys" @@ -18,6 +19,7 @@ Feature: Testing the Surveys API And the response has a "result" property And the response has a "result.id" property And the type of the "result.id" property is "numeric" + And the "result.color" property equals "#A51A1A" And the "result.disabled" property is false And the "result.require_approval" property is true And the "result.require_approval" property is true @@ -42,7 +44,7 @@ Feature: Testing the Surveys API "disabled": 0, "require_approval": 0, "everyone_can_create": 0, - "color": null, + "color": "#A51A1A", "hide_author": 0, "hide_time": 0, "hide_location": 0, @@ -528,6 +530,7 @@ Feature: Testing the Surveys API And the "result.disabled" property is false And the "result.require_approval" property is false And the "result.everyone_can_create" property is false + And the "result.color" property equals "#A51A1A" And the "result.translations.es.name" property equals "ES Test Form has been updated name" And the "result.tasks.0.label" property equals "Main task 1 updated" And the "result.tasks.0.translations.es.label" property equals "ES Main task 1" diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 488ed0d363..845a93aef3 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -54,7 +54,6 @@ public function index() return new SurveyCollection(Survey::all()); }//end index() - /** * Display the specified resource. * diff --git a/v4/Models/Survey.php b/v4/Models/Survey.php index 9568139cde..84788b9885 100644 --- a/v4/Models/Survey.php +++ b/v4/Models/Survey.php @@ -418,4 +418,27 @@ public function translations() { return $this->morphMany('v4\Models\Translation', 'translatable'); }//end translations() + + /** + * Set the user's first name. + * + * @param string $value + * @return void + */ + public function getColorAttribute($value) + { + return $value ? "#" . $value : $value; + } + /** + * Set the user's first name. + * + * @param string $value + * @return void + */ + public function setColorAttribute($value) + { + if (isset($value)) { + $this->attributes['color'] = ltrim($value, '#'); + } + } }//end class From 21bc8a38971061b87f432795fef69fca5af4c97d Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 18 May 2020 23:21:21 -0300 Subject: [PATCH 36/39] Add categories features --- bootstrap/lumen.php | 1 + resources/lang/en/fields.php | 3 + resources/lang/en/validation.php | 2 + resources/lang/es/fields.php | 3 + resources/lang/es/validation.php | 2 + tests/integration/v4/tags.v4.feature | 514 ++++++++++++--------- v4/Http/Controllers/CategoryController.php | 27 +- v4/Models/Category.php | 130 +++++- v4/Policies/CategoryPolicy.php | 2 +- v4/routes/web.php | 10 + 10 files changed, 432 insertions(+), 262 deletions(-) diff --git a/bootstrap/lumen.php b/bootstrap/lumen.php index c3b1262399..6d66e27024 100644 --- a/bootstrap/lumen.php +++ b/bootstrap/lumen.php @@ -96,6 +96,7 @@ $app->register(Sentry\SentryLaravel\SentryLumenServiceProvider::class); $app->register(v4\Providers\MorphServiceProvider::class); + /* |-------------------------------------------------------------------------- | Load The Application Routes diff --git a/resources/lang/en/fields.php b/resources/lang/en/fields.php index 14b23d7b32..f2d0fb9300 100644 --- a/resources/lang/en/fields.php +++ b/resources/lang/en/fields.php @@ -7,7 +7,10 @@ 'hide_author' => 'Hide author', 'hide_location' => 'Hide location', 'hide_time' => 'Hide time', + 'slug' => 'Slug', + 'tag' => 'Tag', 'targeted_survey' => 'Targeted survey', + 'parent_id' => 'Parent category', 'tasks.label' => 'Task label', 'tasks.type' => 'Task type', 'tasks.priority' => 'Priority', diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index d3d8faeaaa..43bfec7712 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -26,4 +26,6 @@ 'regex' => ':field does not match the required format', 'url' => ':field must be a url', 'failedToValidate' => 'Failed to validate %s entity', + 'exists' => ':field must exist', + 'unique' => ':field must be unique', ); diff --git a/resources/lang/es/fields.php b/resources/lang/es/fields.php index 5dbe259950..68198a5397 100644 --- a/resources/lang/es/fields.php +++ b/resources/lang/es/fields.php @@ -7,7 +7,10 @@ 'hide_author' => 'Ocultar autor', 'hide_location' => 'Ocultar ubicación', 'hide_time' => 'Ocultar horario', + 'slug' => 'Slug', + 'tag' => 'Categoria', 'targeted_survey' => 'Encuesta guiada', + 'parent_id' => 'Categoría padre', 'tasks.label' => 'Etiqueta de tarea', 'tasks.type' => 'Tipo de tarea', 'tasks.priority' => 'Prioridad de tarea', diff --git a/resources/lang/es/validation.php b/resources/lang/es/validation.php index c95520ee36..3f10d9069a 100644 --- a/resources/lang/es/validation.php +++ b/resources/lang/es/validation.php @@ -27,4 +27,6 @@ 'url' => ':field debe ser una URL', 'failedToValidate' => 'Falla al validar la entidad %s', 'failedToCreateContact' => 'No se logró crear el contacto. Resultado: %s', + 'exists' => ':field debe existir', + 'unique' => ':field debe ser un valor único', ); diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature index 5564a42572..7b7c3dc2d4 100644 --- a/tests/integration/v4/tags.v4.feature +++ b/tests/integration/v4/tags.v4.feature @@ -1,211 +1,261 @@ @tagsFixture @rolesEnabled Feature: Testing the Categories API -# Scenario: Creating a new Tag -# Given that I want to make a new "Category" -# And that the api_url is "api/v4" -# And that the request "data" is: -# """ -# { -# "parent_id":1, -# "tag":"Boxes", -# "slug":"boxes", -# "description":"Is this a box? Awesome", -# "type":"category", -# "priority":1, -# "color":"00ff00", -# "role": ["admin", "user"] -# } -# """ -# When I request "/categories" -# Then the response is JSON -# And the response has a "result.id" property -# And the type of the "result.id" property is "numeric" -# And the "result.tag" property equals "Boxes" -# And the "result.slug" property equals "boxes" -# And the "result.description" property equals "Is this a box? Awesome" -# And the "result.color" property equals "#00ff00" -# And the "result.priority" property equals "1" -# And the "result.type" property equals "category" -# And the response has a "role" property -# And the "result.parent.id" property equals "1" -# Then the guzzle status code should be 200 -# -# Scenario: Creating a duplicate tag -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Duplicate", -# "type":"category", -# "priority":1 -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 422 -# -# Scenario: Creating a tag with a duplicate slug -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Something", -# "slug":"duplicate", -# "type":"category", -# "priority":1 -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 422 -# -# Scenario: Creating a tag with a long name fails -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long, Really really really really really long", -# "type":"category" -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 422 -# -# Scenario: Check slug is generated on new tag -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"My magical tag", -# "type":"category", -# "priority":1 -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the type of the "id" property is "numeric" -# And the response has a "slug" property -# And the "slug" property equals "my-magical-tag" -# Then the guzzle status code should be 200 -# -# Scenario: Check hash on color input has no effect when creating tag -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Another tag", -# "type":"category", -# "priority":1, -# "color":"#00ff00" -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the type of the "id" property is "numeric" -# And the "color" property equals "#00ff00" -# Then the guzzle status code should be 200 -# -# Scenario: Creating a tag with non-existent parent fails -# Given that I want to make a new "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Superduper tag", -# "type":"category", -# "priority":1, -# "parent_id":10001 -# } -# """ -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 422 -# -# Scenario: Updating a Tag -# Given that I want to update a "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Updated", -# "slug":"updated", -# "type":"status", -# "priority":1 -# } -# """ -# And that its "id" is "1" -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the type of the "id" property is "numeric" -# And the "id" property equals "1" -# And the response has a "tag" property -# And the "tag" property equals "Updated" -# Then the guzzle status code should be 200 -# -# Scenario: Updating a non-existent Tag -# Given that I want to update a "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Updated", -# "slug":"updated", -# "type":"varchar", -# "priority":1 -# } -# """ -# And that its "id" is "40" -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 404 -# -# @resetFixture -# Scenario: Updating Tag Role Restrictions -# Given that I want to update a "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Change Role", -# "slug":"change-role", -# "type":"status", -# "role":["user"] -# } -# """ -# And that its "id" is "1" -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the "id" property equals "1" -# And the response has a "role" property -# And the "role.0" property equals "user" -# Then the guzzle status code should be 200 -# -# Scenario: Removing Tag Role Restrictions -# Given that I want to update a "Tag" -# And that the request "data" is: -# """ -# { -# "tag":"Change Role", -# "slug":"change-role", -# "type":"status", -# "role":[] -# } -# """ -# And that its "id" is "1" -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the "id" property equals "1" -# And the response has a "role" property -# And the "role" property is empty -# Then the guzzle status code should be 200 -# + Scenario: Creating a new Tag + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"Boxes", + "slug":"boxes", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.tag" property equals "Boxes" + And the "result.slug" property equals "boxes" + And the "result.description" property equals "Is this a box? Awesome" + And the "result.color" property equals "#00ff00" + And the "result.priority" property equals "1" + And the "result.type" property equals "category" + And the response has a "result.role" property + And the "result.parent.id" property equals "1" + Then the guzzle status code should be 201 + + Scenario: Creating a duplicate tag + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"Boxes", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "slug" property + And the "slug.0" property equals "Slug must be unique" + And the response has a "tag" property + And the "tag.0" property equals "Tag must be unique" + Then the guzzle status code should be 422 + + Scenario: Creating a tag with a duplicate slug + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"Boxes are fun", + "slug": "boxes", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "slug" property + And the "slug.0" property equals "Slug must be unique" + Then the guzzle status code should be 422 + Scenario: Creating a tag with a long name fails + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome Is this a box? Awesome", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "tag" property + And the "tag.0" property equals "Tag must not exceed 255 characters long" + Then the guzzle status code should be 422 + + Scenario: Check slug is generated on new tag + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"I expect tags", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the response has a "result.slug" property + And the "result.slug" property equals "i-expect-tags" + Then the guzzle status code should be 201 + + Scenario: Check hash on color input has no effect when creating tag + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"I expect tags oo", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"#00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.color" property equals "#00ff00" + Then the guzzle status code should be 201 + + Scenario: Creating a tag with non-existent parent fails + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":123456, + "tag":"I expect tags", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"#00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "parent_id" property + And the "parent_id.0" property equals "Parent category must exist" + Then the guzzle status code should be 422 + + Scenario: Updating a Tag + Given that I want to update a "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "tag":"Updated", + "slug":"updated", + "type":"status", + "priority":1 + } + """ + And that its "id" is "1" + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.id" property equals "1" + And the response has a "result.tag" property + And the "result.tag" property equals "Updated" + Then the guzzle status code should be 200 + + Scenario: Updating a non-existent Tag + Given that I want to update a "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "tag":"Updated", + "slug":"updated", + "type":"varchar", + "priority":1 + } + """ + And that its "id" is "40" + When I request "/categories" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 404 + + @resetFixture + Scenario: Updating Tag Role Restrictions + Given that I want to update a "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "tag":"Change Role", + "slug":"change-role", + "type":"status", + "role":["user"] + } + """ + And that its "id" is "1" + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the "result.id" property equals "1" + And the response has a "result.role" property + And the "result.role" property count is "1" + And the "result.role.0" property equals "user" + Then the guzzle status code should be 200 + + Scenario: Removing Tag Role Restrictions + Given that I want to update a "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "tag":"Change Role", + "slug":"change-role", + "type":"status", + "role":[] + } + """ + And that its "id" is "1" + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the "result.id" property equals "1" + And the response has a "result.role" property + And the "result.role" property is empty + Then the guzzle status code should be 200 + @resetFixture Scenario: Listing All Tags available to admins Given that I want to get all "Categories" @@ -268,29 +318,35 @@ Feature: Testing the Categories API # And the "count" property equals "1" # Then the guzzle status code should be 200 # -# Scenario: Finding a Tag -# Given that I want to find a "Tag" -# And that its "id" is "1" -# When I request "/tags" -# Then the response is JSON -# And the response has a "id" property -# And the type of the "id" property is "numeric" -# Then the guzzle status code should be 200 -# -# Scenario: Finding a non-existent Tag -# Given that I want to find a "Tag" -# And that its "id" is "35" -# When I request "/tags" -# Then the response is JSON -# And the response has a "errors" property -# Then the guzzle status code should be 404 -# -# Scenario: Deleting a Tag -# Given that I want to delete a "Tag" -# And that its "id" is "1" -# When I request "/tags" -# Then the guzzle status code should be 200 -# + Scenario: Finding a Tag + Given that I want to find a "Category" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + And that its "id" is "1" + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + Then the guzzle status code should be 200 + + Scenario: Finding a non-existent Tag + Given that I want to find a "Category" + And that the oauth token is "testbasicuser" + And that the api_url is "api/v4" + And that its "id" is "1333" + When I request "/categories" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 404 + + Scenario: Deleting a Tag + Given that I want to delete a "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that its "id" is "1" + When I request "/categories" + Then the guzzle status code should be 200 + # @resetFixture # Scenario: Deleting a tag removes it from attribute options # Given that I want to delete a "Tag" diff --git a/v4/Http/Controllers/CategoryController.php b/v4/Http/Controllers/CategoryController.php index 851b499aee..9c125469e3 100644 --- a/v4/Http/Controllers/CategoryController.php +++ b/v4/Http/Controllers/CategoryController.php @@ -72,11 +72,16 @@ public function store(Request $request) if ($user) { $this->authorize('store', Category::class); } + $input = $request->input(); + $input['slug'] = Category::makeSlug($input['slug'] ?? $input['tag']); + $category = new Category(); + if (!$category->validate($input)) { + return response()->json($category->errors, 422); + } - $this->validate($request, Category::getRules(), Category::validationMessages()); $category = Category::create( array_merge( - $request->input(), + $input, [ 'created' => time(), ] @@ -131,6 +136,8 @@ private function saveTranslations($input, int $translatable_id, string $type) public function update(int $id, Request $request) { $category = Category::find($id); + + if (!$category) { return response()->json( [ @@ -142,21 +149,13 @@ public function update(int $id, Request $request) 404 ); } - $this->authorize('update', $category); - if (!$category) { - return response()->json( - [ - 'errors' => [ - 'error' => 404, - 'message' => 'Not found', - ], - ], - 404 - ); + + $input = $request->input(); + if (!$category->validate($input)) { + return response()->json($category->errors, 422); } - $this->validate($request, Category::getRules(), Category::validationMessages()); $category->update($request->input()); $this->updateTranslations($request->input('translations'), $category->id, 'category'); return new CategoryResource($category); diff --git a/v4/Models/Category.php b/v4/Models/Category.php index e8389db916..78f52e41a4 100644 --- a/v4/Models/Category.php +++ b/v4/Models/Category.php @@ -4,10 +4,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Validator; use Ushahidi\Core\Entity\Permission; class Category extends Model { + public $errors; /** * Add eloquent style timestamps * @@ -80,10 +82,18 @@ class Category extends Model public static function validationMessages() { return [ + 'parent_id.exists' => trans( + 'validation.exists', + ['field' => trans('fields.parent_id')] + ), 'tag.required' => trans( 'validation.not_empty', ['field' => trans('fields.tag')] ), + 'tag.unique' => trans( + 'validation.unique', + ['field' => trans('fields.tag')] + ), 'tag.min' => trans( 'validation.min_length', [ @@ -113,6 +123,10 @@ public static function validationMessages() 'field' => trans('fields.slug'), ] ), + 'slug.unique' => trans( + 'validation.unique', + ['field' => trans('fields.slug')] + ), 'type.required' => trans( 'validation.not_empty', ['field' => trans('fields.type')] @@ -140,24 +154,25 @@ public static function validationMessages() * Return all validation rules * @return array */ - protected static function getRules() + public function getRules() { return [ - // 'parent_id' = [ - // [[$this->repo, 'doesTagExist'], [':value']], - // ] - 'tag' => [ + 'parent_id' => [ + 'exists:tags,id' + ], + 'tag' => [ 'required', 'min:2', 'max:255', - 'regex:/^[\pL\pN\pP ]++$/uD' - ], - 'slug' => [ + 'regex:/^[\pL\pN\pP ]++$/uD', + Rule::unique('tags')->ignore($this->id) + ], + 'slug' => [ 'required', 'min:2', - // [[$this->repo, 'isSlugAvailable'], [':value']], - ], - 'type' => [ + Rule::unique('tags')->ignore($this->id) + ], + 'type' => [ 'required', Rule::in([ 'category', @@ -166,22 +181,22 @@ protected static function getRules() // 'min:2', // 'max:255', // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, - ], - 'description' => [ + ], + 'description' => [ 'regex:/^[\pL\pN\pP ]++$/uD' // 'min:2', // 'max:255', // 'regex:'.LegacyValidator::REGEX_STANDARD_TEXT, - ], + ], // 'color' => [ // ['color'], // ] - 'icon' => [ + 'icon' => [ 'regex:/^[\pL\s\_\-]++$/uD' - ], - 'priority' => [ + ], + 'priority' => [ 'numeric' - ], + ], // 'role' => [ // [[$this->role_repo, 'exists'], [':value']], // [[$this->repo, 'isRoleValid'], [':validation', ':fulldata']] @@ -275,4 +290,83 @@ public function scopeAllowed($query) return $q; } + + /** + * Get the category's color format + * + * @param string $value + * @return void + */ + public function getColorAttribute($value) + { + return $value ? "#" . $value : $value; + } + /** + * Set the category's color format + * + * @param string $value + * @return void + */ + public function setColorAttribute($value) + { + if (isset($value)) { + $this->attributes['color'] = ltrim($value, '#'); + } + } + + /** + * Get the category's slug + * + * @param string $value + * @return void + */ + public function getSlugAttribute($value) + { + return $value; + } + /** + * Set the category's slug format + * + * @param string $value + * @return void + */ + public function setSlugAttribute($value) + { + if (isset($value) && (!isset($this->attributes['slug']))) { + $value = self::makeSlug($value); + $this->attributes['slug'] = $value; + } + } + public static function makeSlug($value) + { + // Make it lowercase + $value = mb_strtolower($value, 'utf-8'); + + // .. anything not the separator, letters, numbers or whitespace is replaced + $value = preg_replace('/[^\pL\pN\-\s]+/u', '', $value); + + // .. replace whitespace and multiple separator chars with a single separator + $value = preg_replace('/[\-\s]+/u', '-', $value); + + // ... and replace spaces with hypens + $value = str_replace(' ', '-', $value); + return $value; + } + + public function validate($data) + { + $v = Validator::make($data, $this->getRules(), self::validationMessages()); + // check for failure + if (!$v->fails()) { + return true; + } + // set errors and return false + $this->errors = $v->errors(); + return false; + } + + public function errors() + { + return $this->errors; + } }//end class diff --git a/v4/Policies/CategoryPolicy.php b/v4/Policies/CategoryPolicy.php index c6221e45ac..8b357637fe 100644 --- a/v4/Policies/CategoryPolicy.php +++ b/v4/Policies/CategoryPolicy.php @@ -111,7 +111,7 @@ public function isAllowed($entity, $privilege) } // First check whether there is a role with the right permissions - if ($this->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { + if ($authorizer->acl->hasPermission($user, Permission::MANAGE_SETTINGS)) { return true; } diff --git a/v4/routes/web.php b/v4/routes/web.php index d83b79989d..031438af5c 100644 --- a/v4/routes/web.php +++ b/v4/routes/web.php @@ -28,6 +28,16 @@ $router->get('/{id}', 'CategoryController@show'); }); + // Restricted access + $router->group([ + 'prefix' => 'categories', + 'middleware' => ['auth:api', 'scope:tags'] + ], function () use ($router) { + $router->post('/', 'CategoryController@store'); + $router->put('/{id}', 'CategoryController@update'); + $router->delete('/{id}', 'CategoryController@delete'); + }); + // Restricted access $router->group([ 'prefix' => 'surveys', From f7aabf6952d88a29c82005d1c33e3dd61fdb0f1d Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 19 May 2020 18:09:11 -0300 Subject: [PATCH 37/39] Add more tests and fix an issue on survey updates (return full hidrated stages with updates) --- .../v4/forms/fieldupdates.v4.feature | 996 ++++++++++++++++++ tests/integration/v4/forms/forms.v4.feature | 509 --------- v4/Http/Controllers/SurveyController.php | 2 + 3 files changed, 998 insertions(+), 509 deletions(-) create mode 100644 tests/integration/v4/forms/fieldupdates.v4.feature diff --git a/tests/integration/v4/forms/fieldupdates.v4.feature b/tests/integration/v4/forms/fieldupdates.v4.feature new file mode 100644 index 0000000000..60a66804be --- /dev/null +++ b/tests/integration/v4/forms/fieldupdates.v4.feature @@ -0,0 +1,996 @@ +@rolesEnabled +Feature: Testing the Surveys API + Scenario: Updating a Survey + Given that I want to update a "Survey" + And that the oauth token is "testmanager" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "id": 1, + "name": "Test Form has been updated name", + "parent_id": null, + "description": "Testing form is updated desc", + "type": "report", + "disabled": 0, + "require_approval": 0, + "everyone_can_create": 0, + "color": "#A51A1A", + "hide_author": 0, + "hide_time": 0, + "hide_location": 0, + "targeted_survey": 0, + "base_language": "en", + "translations": { + "es": { + "name": "ES Test Form has been updated name", + "description": "ES Testing form is updated desc" + } + }, + "tasks": [ + { + "id": 1, + "form_id": 1, + "label": "Main task 1 updated", + "priority": 1, + "required": 0, + "type": "post", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 1, + "key": "test_varchar", + "label": "Test varchar", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test varchar" + } + } + }, + { + "id": 2, + "key": "test_point", + "label": "Test point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test point" + } + } + }, + { + "id": 3, + "key": "full_name", + "label": "Full Name", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Full Name" + } + } + }, + { + "id": 4, + "key": "description", + "label": "Description", + "instructions": null, + "input": "textarea", + "type": "description", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Description" + } + } + }, + { + "id": 5, + "key": "date_of_birth", + "label": "Date of birth", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 3, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Date of birth" + } + } + }, + { + "id": 6, + "key": "missing_date", + "label": "Missing date", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 4, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Missing date" + } + } + }, + { + "id": 7, + "key": "last_location", + "label": "Last Location", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 1, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location" + } + } + }, + { + "id": 8, + "key": "last_location_point", + "label": "Last Location (point)", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location (point)" + } + } + }, + { + "id": 9, + "key": "geometry_test", + "label": "Geometry test", + "instructions": null, + "input": "text", + "type": "geometry", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Geometry test" + } + } + }, + { + "id": 10, + "key": "missing_status", + "label": "Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Status" + } + } + }, + { + "id": 11, + "key": "links", + "label": "Links", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Links" + } + } + }, + { + "id": 12, + "key": "second_point", + "label": "Second Point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Second Point" + } + } + }, + { + "id": 14, + "key": "media_test", + "label": "Media Test", + "instructions": null, + "input": "upload", + "type": "media", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Media Test" + } + } + }, + { + "id": 15, + "key": "possible_actions", + "label": "Possible actions", + "instructions": null, + "input": "checkbox", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "ground_search", + "medical_evacuation" + ], + "cardinality": 0, + "config": [], + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Possible actions" + } + } + }, + { + "id": 17, + "key": "title", + "label": "Title", + "instructions": null, + "input": "text", + "type": "title", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Title" + } + } + }, + { + "id": 25, + "key": "markdown", + "label": "Test markdown", + "instructions": null, + "input": "text", + "type": "markdown", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test markdown" + } + } + }, + { + "id": 26, + "key": "tags1", + "label": "Categories", + "instructions": null, + "input": "tags", + "type": "tags", + "required": 0, + "default": null, + "priority": 3, + "options": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Categories", + "options": [ + "ES 1", + "ES 2", + "ES 3", + "ES 4", + "ES 5", + "ES 6", + "ES 7" + ] + } + } + } + ], + "translations": { + "es": { + "label": "ES Main task 1" + } + } + }, + { + "id": 2, + "form_id": 1, + "label": "2nd step", + "priority": 2, + "required": 0, + "type": "task", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 13, + "key": "person_status", + "label": "Person Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 2, + "translations": { + "es": { + "label": "ES Person Status" + } + } + } + ], + "translations": { + "es": { + "label": "ES 2nd step" + } + } + }, + { + "id": 3, + "form_id": 1, + "label": "3rd step", + "priority": 3, + "required": 0, + "type": "task", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [], + "translations": [] + } + ] + } + """ + And that its "id" is "1" + When I request "/surveys" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.id" property equals "1" + And the response has a "result.name" property + And the "result.name" property equals "Test Form has been updated name" + And the "result.disabled" property is false + And the "result.require_approval" property is false + And the "result.everyone_can_create" property is false + And the "result.color" property equals "#A51A1A" + And the "result.translations.es.name" property equals "ES Test Form has been updated name" + And the "result.tasks" property count is "3" + And the "result.tasks.0.fields" property count is "17" + And the "result.tasks.0.label" property equals "Main task 1 updated" + And the "result.tasks.0.translations.es.label" property equals "ES Main task 1" + And the "result.tasks.0.fields.0.label" property equals "Test varchar" + And the "result.tasks.0.fields.0.translations.es.label" property equals "ES Test varchar" + And the "result.tasks.1.fields" property count is "1" + And the "result.tasks.1.translations.es.label" property equals "ES 2nd step" + And the "result.tasks.1.fields.0.translations.es.label" property equals "ES Person Status" + And the "result.tasks.2.fields" property count is "0" + Then the guzzle status code should be 200 + Scenario: Update survey including removing one task and a field from another task + Given that I want to update a "Survey" + And that the oauth token is "testmanager" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "id": 1, + "name": "UPDATED Form has been updated name", + "parent_id": null, + "description": "UPDATED Testing form is updated desc", + "type": "report", + "disabled": 0, + "require_approval": 0, + "everyone_can_create": 0, + "color": "#A51A1B", + "hide_author": 0, + "hide_time": 0, + "hide_location": 0, + "targeted_survey": 0, + "base_language": "en", + "translations": { + "es": { + "name": "UPDATED ES Test Form has been updated name", + "description": "UPDATED ES Testing form is updated desc" + } + }, + "tasks": [ + { + "id": 1, + "form_id": 1, + "label": "Main task 1 updated", + "priority": 1, + "required": 0, + "type": "post", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 2, + "key": "test_point", + "label": "Test point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test point" + } + } + }, + { + "id": 3, + "key": "full_name", + "label": "Full Name", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 1, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Full Name" + } + } + }, + { + "id": 4, + "key": "description", + "label": "Description", + "instructions": null, + "input": "textarea", + "type": "description", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Description" + } + } + }, + { + "id": 5, + "key": "date_of_birth", + "label": "Date of birth", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 3, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Date of birth" + } + } + }, + { + "id": 6, + "key": "missing_date", + "label": "Missing date", + "instructions": null, + "input": "date", + "type": "datetime", + "required": 0, + "default": null, + "priority": 4, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Missing date" + } + } + }, + { + "id": 7, + "key": "last_location", + "label": "Last Location", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 1, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location" + } + } + }, + { + "id": 8, + "key": "last_location_point", + "label": "Last Location (point)", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Last Location (point)" + } + } + }, + { + "id": 9, + "key": "geometry_test", + "label": "Geometry test", + "instructions": null, + "input": "text", + "type": "geometry", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Geometry test" + } + } + }, + { + "id": 10, + "key": "missing_status", + "label": "Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Status" + } + } + }, + { + "id": 11, + "key": "links", + "label": "Links", + "instructions": null, + "input": "text", + "type": "varchar", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Links" + } + } + }, + { + "id": 12, + "key": "second_point", + "label": "Second Point", + "instructions": null, + "input": "location", + "type": "point", + "required": 0, + "default": null, + "priority": 5, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Second Point" + } + } + }, + { + "id": 14, + "key": "media_test", + "label": "Media Test", + "instructions": null, + "input": "upload", + "type": "media", + "required": 0, + "default": null, + "priority": 7, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Media Test" + } + } + }, + { + "id": 15, + "key": "possible_actions", + "label": "Possible actions", + "instructions": null, + "input": "checkbox", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "ground_search", + "medical_evacuation" + ], + "cardinality": 0, + "config": [], + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Possible actions" + } + } + }, + { + "id": 17, + "key": "title", + "label": "Title", + "instructions": null, + "input": "text", + "type": "title", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Title" + } + } + }, + { + "id": 25, + "key": "markdown", + "label": "Test markdown", + "instructions": null, + "input": "text", + "type": "markdown", + "required": 0, + "default": null, + "priority": 0, + "options": null, + "cardinality": 1, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Test markdown" + } + } + }, + { + "id": 26, + "key": "tags1", + "label": "Categories", + "instructions": null, + "input": "tags", + "type": "tags", + "required": 0, + "default": null, + "priority": 3, + "options": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 1, + "translations": { + "es": { + "label": "ES Categories", + "options": [ + "ES 1", + "ES 2", + "ES 3", + "ES 4", + "ES 5", + "ES 6", + "ES 7" + ] + } + } + } + ], + "translations": { + "es": { + "label": "ES Main task 1" + } + } + }, + { + "id": 2, + "form_id": 1, + "label": "2nd step", + "priority": 2, + "required": 0, + "type": "task", + "description": null, + "show_when_published": 1, + "task_is_internal_only": 0, + "fields": [ + { + "id": 13, + "key": "person_status", + "label": "Person Status", + "instructions": null, + "input": "select", + "type": "varchar", + "required": 0, + "default": null, + "priority": 5, + "options": [ + "information_sought", + "is_note_author", + "believed_alive", + "believed_missing", + "believed_dead" + ], + "cardinality": 0, + "config": null, + "response_private": 0, + "form_stage_id": 2, + "translations": { + "es": { + "label": "ES Person Status" + } + } + } + ], + "translations": { + "es": { + "label": "ES 2nd step" + } + } + } + ] + } + """ + And that its "id" is "1" + When I request "/surveys" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.id" property equals "1" + And the response has a "result.name" property + And the "result.name" property equals "UPDATED Form has been updated name" + And the "result.description" property equals "UPDATED Testing form is updated desc" + And the "result.disabled" property is false + And the "result.require_approval" property is false + And the "result.everyone_can_create" property is false + And the "result.color" property equals "#A51A1B" + And the "result.translations.es.name" property equals "UPDATED ES Test Form has been updated name" + And the "result.tasks" property count is "2" + And the "result.tasks.0.fields" property count is "16" + And the "result.tasks.0.label" property equals "Main task 1 updated" + And the "result.tasks.0.translations.es.label" property equals "ES Main task 1" + And the "result.tasks.0.fields.0.label" property equals "Test point" + And the "result.tasks.0.fields.0.translations.es.label" property equals "ES Test point" + And the "result.tasks.1.fields" property count is "1" + And the "result.tasks.1.translations.es.label" property equals "ES 2nd step" + And the "result.tasks.1.fields.0.translations.es.label" property equals "ES Person Status" + Then the guzzle status code should be 200 diff --git a/tests/integration/v4/forms/forms.v4.feature b/tests/integration/v4/forms/forms.v4.feature index cd362b8dd5..1625871db2 100644 --- a/tests/integration/v4/forms/forms.v4.feature +++ b/tests/integration/v4/forms/forms.v4.feature @@ -29,515 +29,6 @@ Feature: Testing the Surveys API And the "result.can_create" property is empty Then the guzzle status code should be 201 - Scenario: Updating a Survey - Given that I want to update a "Survey" - And that the oauth token is "testmanager" - And that the api_url is "api/v4" - And that the request "data" is: - """ - { - "id": 1, - "name": "Test Form has been updated name", - "parent_id": null, - "description": "Testing form is updated desc", - "type": "report", - "disabled": 0, - "require_approval": 0, - "everyone_can_create": 0, - "color": "#A51A1A", - "hide_author": 0, - "hide_time": 0, - "hide_location": 0, - "targeted_survey": 0, - "base_language": "en", - "translations": { - "es": { - "name": "ES Test Form has been updated name", - "description": "ES Testing form is updated desc" - } - }, - "tasks": [ - { - "id": 1, - "form_id": 1, - "label": "Main task 1 updated", - "priority": 1, - "required": 0, - "type": "post", - "description": null, - "show_when_published": 1, - "task_is_internal_only": 0, - "fields": [ - { - "id": 1, - "key": "test_varchar", - "label": "Test varchar", - "instructions": null, - "input": "text", - "type": "varchar", - "required": 0, - "default": null, - "priority": 1, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Test varchar" - } - } - }, - { - "id": 2, - "key": "test_point", - "label": "Test point", - "instructions": null, - "input": "location", - "type": "point", - "required": 0, - "default": null, - "priority": 1, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Test point" - } - } - }, - { - "id": 3, - "key": "full_name", - "label": "Full Name", - "instructions": null, - "input": "text", - "type": "varchar", - "required": 0, - "default": null, - "priority": 1, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Full Name" - } - } - }, - { - "id": 4, - "key": "description", - "label": "Description", - "instructions": null, - "input": "textarea", - "type": "description", - "required": 0, - "default": null, - "priority": 0, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Description" - } - } - }, - { - "id": 5, - "key": "date_of_birth", - "label": "Date of birth", - "instructions": null, - "input": "date", - "type": "datetime", - "required": 0, - "default": null, - "priority": 3, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Date of birth" - } - } - }, - { - "id": 6, - "key": "missing_date", - "label": "Missing date", - "instructions": null, - "input": "date", - "type": "datetime", - "required": 0, - "default": null, - "priority": 4, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Missing date" - } - } - }, - { - "id": 7, - "key": "last_location", - "label": "Last Location", - "instructions": null, - "input": "text", - "type": "varchar", - "required": 1, - "default": null, - "priority": 5, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Last Location" - } - } - }, - { - "id": 8, - "key": "last_location_point", - "label": "Last Location (point)", - "instructions": null, - "input": "location", - "type": "point", - "required": 0, - "default": null, - "priority": 5, - "options": null, - "cardinality": 0, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Last Location (point)" - } - } - }, - { - "id": 9, - "key": "geometry_test", - "label": "Geometry test", - "instructions": null, - "input": "text", - "type": "geometry", - "required": 0, - "default": null, - "priority": 5, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Geometry test" - } - } - }, - { - "id": 10, - "key": "missing_status", - "label": "Status", - "instructions": null, - "input": "select", - "type": "varchar", - "required": 0, - "default": null, - "priority": 5, - "options": [ - "information_sought", - "is_note_author", - "believed_alive", - "believed_missing", - "believed_dead" - ], - "cardinality": 0, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Status" - } - } - }, - { - "id": 11, - "key": "links", - "label": "Links", - "instructions": null, - "input": "text", - "type": "varchar", - "required": 0, - "default": null, - "priority": 7, - "options": null, - "cardinality": 0, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Links" - } - } - }, - { - "id": 12, - "key": "second_point", - "label": "Second Point", - "instructions": null, - "input": "location", - "type": "point", - "required": 0, - "default": null, - "priority": 5, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Second Point" - } - } - }, - { - "id": 14, - "key": "media_test", - "label": "Media Test", - "instructions": null, - "input": "upload", - "type": "media", - "required": 0, - "default": null, - "priority": 7, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Media Test" - } - } - }, - { - "id": 15, - "key": "possible_actions", - "label": "Possible actions", - "instructions": null, - "input": "checkbox", - "type": "varchar", - "required": 0, - "default": null, - "priority": 5, - "options": [ - "ground_search", - "medical_evacuation" - ], - "cardinality": 0, - "config": [], - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Possible actions" - } - } - }, - { - "id": 17, - "key": "title", - "label": "Title", - "instructions": null, - "input": "text", - "type": "title", - "required": 0, - "default": null, - "priority": 0, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Title" - } - } - }, - { - "id": 25, - "key": "markdown", - "label": "Test markdown", - "instructions": null, - "input": "text", - "type": "markdown", - "required": 0, - "default": null, - "priority": 0, - "options": null, - "cardinality": 1, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Test markdown" - } - } - }, - { - "id": 26, - "key": "tags1", - "label": "Categories", - "instructions": null, - "input": "tags", - "type": "tags", - "required": 0, - "default": null, - "priority": 3, - "options": [ - "1", - "2", - "3", - "4", - "5", - "6", - "7" - ], - "cardinality": 0, - "config": null, - "response_private": 0, - "form_stage_id": 1, - "translations": { - "es": { - "label": "ES Categories", - "options": [ - "ES 1", - "ES 2", - "ES 3", - "ES 4", - "ES 5", - "ES 6", - "ES 7" - ] - } - } - } - ], - "translations": { - "es": { - "label": "ES Main task 1" - } - } - }, - { - "id": 2, - "form_id": 1, - "label": "2nd step", - "priority": 2, - "required": 0, - "type": "task", - "description": null, - "show_when_published": 1, - "task_is_internal_only": 0, - "fields": [ - { - "id": 13, - "key": "person_status", - "label": "Person Status", - "instructions": null, - "input": "select", - "type": "varchar", - "required": 0, - "default": null, - "priority": 5, - "options": [ - "information_sought", - "is_note_author", - "believed_alive", - "believed_missing", - "believed_dead" - ], - "cardinality": 0, - "config": null, - "response_private": 0, - "form_stage_id": 2, - "translations": { - "es": { - "label": "ES Person Status" - } - } - } - ], - "translations": { - "es": { - "label": "ES 2nd step" - } - } - }, - { - "id": 3, - "form_id": 1, - "label": "3rd step", - "priority": 3, - "required": 0, - "type": "task", - "description": null, - "show_when_published": 1, - "task_is_internal_only": 0, - "fields": [], - "translations": [] - } - ] - } - """ - And that its "id" is "1" - When I request "/surveys" - Then the response is JSON - And the response has a "result.id" property - And the type of the "result.id" property is "numeric" - And the "result.id" property equals "1" - And the response has a "result.name" property - And the "result.name" property equals "Test Form has been updated name" - And the "result.disabled" property is false - And the "result.require_approval" property is false - And the "result.everyone_can_create" property is false - And the "result.color" property equals "#A51A1A" - And the "result.translations.es.name" property equals "ES Test Form has been updated name" - And the "result.tasks.0.label" property equals "Main task 1 updated" - And the "result.tasks.0.translations.es.label" property equals "ES Main task 1" - And the "result.tasks.0.fields.0.label" property equals "Test varchar" - And the "result.tasks.0.fields.0.translations.es.label" property equals "ES Test varchar" - Then the guzzle status code should be 200 - Scenario: Updating a Survey to clear name should fail Given that I want to update a "Survey" And that the api_url is "api/v4" diff --git a/v4/Http/Controllers/SurveyController.php b/v4/Http/Controllers/SurveyController.php index 845a93aef3..379f4d5777 100644 --- a/v4/Http/Controllers/SurveyController.php +++ b/v4/Http/Controllers/SurveyController.php @@ -197,6 +197,8 @@ public function update(int $id, Request $request) ); $this->updateTranslations($request->input('translations'), $survey->id, 'survey'); $this->updateTasks(($request->input('tasks') ?? []), $survey); + $survey->load('tasks'); + return new SurveyResource($survey); }//end update() From fb4fd1809ad8dedc727f6faaff9b591ac548159f Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 20 May 2020 19:51:40 -0300 Subject: [PATCH 38/39] Only check exists for category parent id IF the parent id is filled --- tests/integration/v4/tags.v4.feature | 19 +++++++++++++++++++ v4/Models/Category.php | 4 +--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature index 7b7c3dc2d4..3f3ad49475 100644 --- a/tests/integration/v4/tags.v4.feature +++ b/tests/integration/v4/tags.v4.feature @@ -168,6 +168,25 @@ Feature: Testing the Categories API And the "parent_id.0" property equals "Parent category must exist" Then the guzzle status code should be 422 + Scenario: Creating a tag with empty parent_id works + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "tag":"I expect tags to be here", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"#00ff00", + "role": ["admin", "user"] + } + """ + When I request "/categories" + Then the response is JSON + Then the guzzle status code should be 201 + Scenario: Updating a Tag Given that I want to update a "Category" And that the oauth token is "testadminuser" diff --git a/v4/Models/Category.php b/v4/Models/Category.php index 78f52e41a4..ece200e88a 100644 --- a/v4/Models/Category.php +++ b/v4/Models/Category.php @@ -157,9 +157,7 @@ public static function validationMessages() public function getRules() { return [ - 'parent_id' => [ - 'exists:tags,id' - ], + 'parent_id' => 'sometimes|exists:tags,id', 'tag' => [ 'required', 'min:2', From 3dcc21e3a98fb55f5643ce3c469059898e2471c5 Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 20 May 2020 23:00:31 -0300 Subject: [PATCH 39/39] Add base_language for categories --- ...014822_add_base_language_to_categories.php | 25 +++++++++++ tests/integration/v4/tags.v4.feature | 42 ++++++++++++++++++- v4/Http/Resources/CategoryResource.php | 4 ++ v4/Models/Category.php | 3 +- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 migrations/20200521014822_add_base_language_to_categories.php diff --git a/migrations/20200521014822_add_base_language_to_categories.php b/migrations/20200521014822_add_base_language_to_categories.php new file mode 100644 index 0000000000..93f4a20e2e --- /dev/null +++ b/migrations/20200521014822_add_base_language_to_categories.php @@ -0,0 +1,25 @@ +fetchRow( + "SELECT config_value FROM config WHERE group_name='site' and config_key='language' " + ); + // get two letter lang code + $language = str_before(json_decode($result['config_value']), '-'); + $this->table('tags') + ->addColumn('base_language', 'string', ['null' => false, 'default' => $language]) //es/en + ->update(); + } + + + public function down() + { + $this->table('tags')->removeColumn('base_language'); + } +} diff --git a/tests/integration/v4/tags.v4.feature b/tests/integration/v4/tags.v4.feature index 3f3ad49475..c457b86121 100644 --- a/tests/integration/v4/tags.v4.feature +++ b/tests/integration/v4/tags.v4.feature @@ -1,6 +1,6 @@ @tagsFixture @rolesEnabled Feature: Testing the Categories API - Scenario: Creating a new Tag + Scenario: Creating a new Tag with a base language Given that I want to make a new "Category" And that the oauth token is "testadminuser" And that the api_url is "api/v4" @@ -14,6 +14,7 @@ Feature: Testing the Categories API "type":"category", "priority":1, "color":"00ff00", + "base_language": "en", "role": ["admin", "user"] } """ @@ -27,10 +28,47 @@ Feature: Testing the Categories API And the "result.color" property equals "#00ff00" And the "result.priority" property equals "1" And the "result.type" property equals "category" + And the "result.enabled_languages.default" property equals "en" + And the response has a "result.role" property + And the "result.parent.id" property equals "1" + Then the guzzle status code should be 201 + Scenario: Creating a new Tag with a base language and translation + Given that I want to make a new "Category" + And that the oauth token is "testadminuser" + And that the api_url is "api/v4" + And that the request "data" is: + """ + { + "parent_id":1, + "tag":"Boxes with a translation", + "description":"Is this a box? Awesome", + "type":"category", + "priority":1, + "color":"00ff00", + "base_language": "en", + "role": ["admin", "user"], + "translations": { + "es": { + "tag": "Cajas" + } + } + } + """ + When I request "/categories" + Then the response is JSON + And the response has a "result.id" property + And the type of the "result.id" property is "numeric" + And the "result.tag" property equals "Boxes with a translation" + And the "result.slug" property equals "boxes-with-a-translation" + And the "result.description" property equals "Is this a box? Awesome" + And the "result.color" property equals "#00ff00" + And the "result.priority" property equals "1" + And the "result.type" property equals "category" + And the "result.enabled_languages.default" property equals "en" + And the "result.enabled_languages.available.0" property equals "es" And the response has a "result.role" property And the "result.parent.id" property equals "1" Then the guzzle status code should be 201 - Scenario: Creating a duplicate tag Given that I want to make a new "Category" And that the oauth token is "testadminuser" diff --git a/v4/Http/Resources/CategoryResource.php b/v4/Http/Resources/CategoryResource.php index c0ad46df7f..c6db9a4743 100644 --- a/v4/Http/Resources/CategoryResource.php +++ b/v4/Http/Resources/CategoryResource.php @@ -29,6 +29,10 @@ public function toArray($request) 'children' => $this->children, 'parent' => $this->parent, 'translations' => new TranslationCollection($this->translations), + 'enabled_languages' => [ + 'default'=> $this->base_language, + 'available' => $this->translations->groupBy('language')->keys() + ] ]; } } diff --git a/v4/Models/Category.php b/v4/Models/Category.php index ece200e88a..45e1d80c41 100644 --- a/v4/Models/Category.php +++ b/v4/Models/Category.php @@ -58,7 +58,8 @@ class Category extends Model 'icon', 'description', 'role', - 'priority' + 'priority', + 'base_language' ]; /**