Commit 7fb0694d authored by François Agneray's avatar François Agneray
Browse files

Merge branch '70-add-and-delete-attribute' into 'develop'

Resolve "Add and delete attribute"

Closes #70

See merge request !58
parents c38c7fc4 e6c89ffe
Pipeline #5221 passed with stages
in 2 minutes and 57 seconds
......@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- In progress...
### Added
- #70: Add endpoint to add or delete an attribute
- #69: Add endpoint to list all columns for a table
- #66: Export all attributes (a=all)
- #48: Export to votable format
......
......@@ -129,7 +129,7 @@ $container->set('App\Action\DatasetFamilyAction', function (ContainerInterface $
});
$container->set('App\Action\DatasetListAction', function (ContainerInterface $c) {
return new App\Action\DatasetListAction($c->get('em'), new App\Search\DBALConnectionFactory());
return new App\Action\DatasetListAction($c->get('em'));
});
$container->set('App\Action\DatasetAction', function (ContainerInterface $c) {
......
......@@ -22,7 +22,6 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->map([OPTIONS, GET, POST], '/database', App\Action\DatabaseListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseAction::class);
$group->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class);
$group->map([OPTIONS, GET], '/database/{id}/table/{tname}/column', App\Action\ColumnListAction::class);
$group->map([OPTIONS, GET], '/file-explorer[{fpath:.*}]', App\Action\AdminFileExplorerAction::class);
})->add(new App\Middleware\RouteGuardMiddleware(
boolval($container->get(SETTINGS)['token']['enabled']),
......@@ -53,8 +52,9 @@ $app->group('', function (RouteCollectorProxy $group) {
App\Action\OutputCategoryListAction::class
);
$group->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class);
$group->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class);
$group->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class);
$group->map([OPTIONS, GET, POST], '/dataset/{name}/attribute', App\Action\AttributeListAction::class);
$group->map([OPTIONS, GET], '/dataset/{name}/column', App\Action\ColumnListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class);
$group->map(
[OPTIONS, GET, PUT],
'/dataset/{name}/attribute/{id}/distinct',
......
This diff is collapsed.
......@@ -18,8 +18,6 @@ use Slim\Exception\HttpUnauthorizedException;
use Slim\Exception\HttpForbiddenException;
/**
* Centralize access to the database and some common functions
*
* @author François Agneray <francois.agneray@lam.fr>
* @package App\Action
*/
......
......@@ -36,7 +36,7 @@ final class AttributeAction extends AbstractAction
public function __invoke(Request $request, Response $response, array $args): Response
{
if ($request->getMethod() === OPTIONS) {
return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, OPTIONS');
return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS');
}
$attribute = $this->em->getRepository('App\Entity\Attribute')->findOneBy(
......@@ -61,6 +61,13 @@ final class AttributeAction extends AbstractAction
$payload = json_encode($attribute);
}
if ($request->getMethod() === DELETE) {
$id = $attribute->getId();
$this->em->remove($attribute);
$this->em->flush();
$payload = json_encode(array('message' => 'Attribute with id ' . $id . ' is removed!'));
}
$response->getBody()->write($payload);
return $response;
}
......
......@@ -15,6 +15,9 @@ namespace App\Action;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpBadRequestException;
use App\Entity\Dataset;
use App\Entity\Attribute;
/**
* @author François Agneray <francois.agneray@lam.fr>
......@@ -34,7 +37,7 @@ final class AttributeListAction extends AbstractAction
public function __invoke(Request $request, Response $response, array $args): Response
{
if ($request->getMethod() === OPTIONS) {
return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
}
$dataset = $this->em->find('App\Entity\Dataset', $args['name']);
......@@ -55,7 +58,100 @@ final class AttributeListAction extends AbstractAction
$payload = json_encode($attributes);
}
if ($request->getMethod() === POST) {
$parsedBody = $request->getParsedBody();
$fields = array(
'id',
'name',
'label',
'form_label',
'type',
'criteria_display',
'output_display',
'display_detail',
'order_display'
);
foreach ($fields as $a) {
if ($this->isEmptyField($a, $parsedBody)) {
throw new HttpBadRequestException(
$request,
'Param ' . $a . ' needed to add a new attribute'
);
}
}
$attribute = $this->postAttribute($parsedBody, $dataset);
$payload = json_encode($attribute);
$response = $response->withStatus(201);
}
$response->getBody()->write($payload);
return $response;
}
/**
* Add a new attribute into the metamodel
*
* @param array $parsedBody Contains the values ​​of the new attribute sent by the user
* @param Dataset $dataset The attribute's dataset
*
* @return Attribute
*/
private function postAttribute(array $parsedBody, Dataset $dataset): Attribute
{
$attribute = new Attribute($parsedBody['id'], $dataset);
$attribute->setName($parsedBody['name']);
$attribute->setLabel($parsedBody['label']);
$attribute->setFormLabel($parsedBody['form_label']);
$attribute->setDescription($parsedBody['description']);
$attribute->setOutputDisplay($parsedBody['output_display']);
$attribute->setCriteriaDisplay($parsedBody['criteria_display']);
$attribute->setSearchFlag($parsedBody['search_flag']);
$attribute->setSearchType($parsedBody['search_type']);
$attribute->setOperator($parsedBody['operator']);
$attribute->setType($parsedBody['type']);
$attribute->setMin($parsedBody['min']);
$attribute->setMax($parsedBody['max']);
$attribute->setPlaceholderMin($parsedBody['placeholder_min']);
$attribute->setPlaceholderMax($parsedBody['placeholder_max']);
$attribute->setRenderer($parsedBody['renderer']);
$attribute->setRendererConfig($parsedBody['renderer_config']);
$attribute->setDisplayDetail($parsedBody['display_detail']);
$attribute->setSelected($parsedBody['selected']);
$attribute->setOrderBy($parsedBody['order_by']);
$attribute->setOrderDisplay($parsedBody['order_display']);
$attribute->setDetail($parsedBody['detail']);
$attribute->setRendererDetail($parsedBody['renderer_detail']);
$attribute->setOptions($parsedBody['options']);
$attribute->setVoUtype($parsedBody['vo_utype']);
$attribute->setVoUcd($parsedBody['vo_ucd']);
$attribute->setVoUnit($parsedBody['vo_unit']);
$attribute->setVoDescription($parsedBody['vo_description']);
$attribute->setVoDatatype($parsedBody['vo_datatype']);
$attribute->setVoSize($parsedBody['vo_size']);
if (is_null($parsedBody['id_criteria_family'])) {
$criteriaFamily = null;
} else {
$criteriaFamily = $this->em->find(
'App\Entity\CriteriaFamily',
$parsedBody['id_criteria_family']
);
}
$attribute->setCriteriaFamily($criteriaFamily);
if (is_null($parsedBody['id_output_category'])) {
$outputCategory = null;
} else {
$outputCategory = $this->em->find(
'App\Entity\OutputCategory',
$parsedBody['id_output_category']
);
}
$attribute->setOutputCategory($outputCategory);
$this->em->persist($attribute);
$this->em->flush();
return $attribute;
}
}
......@@ -57,21 +57,21 @@ final class ColumnListAction extends AbstractAction
return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
}
// Search the correct database with primary key
$database = $this->em->find('App\Entity\Database', $args['id']);
$dataset = $this->em->find('App\Entity\Dataset', $args['name']);
// If database is not found 404
if (is_null($database)) {
// Returns HTTP 404 if the dataset is not found
if (is_null($dataset)) {
throw new HttpNotFoundException(
$request,
'Database with id ' . $args['id'] . ' is not found'
'Dataset with name ' . $args['name'] . ' is not found'
);
}
if ($request->getMethod() === GET) {
$database = $dataset->getProject()->getDatabase();
$connection = $this->connectionFactory->create($database);
$sm = $connection->getSchemaManager();
$columns = $this->getColumns($sm, $args['tname']);
$columns = $this->getColumns($sm, $dataset->getTableRef());
$payload = json_encode($columns);
}
......@@ -88,7 +88,10 @@ final class ColumnListAction extends AbstractAction
{
$columns = array();
foreach ($sm->listTableColumns($tableName) as $column) {
$columns[] = $column->getName();
$columns[] = array(
'name' => $column->getName(),
'type' => $column->getType()->getName()
);
}
return $columns;
}
......
......@@ -16,12 +16,9 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
use Doctrine\ORM\EntityManagerInterface;
use App\Search\DBALConnectionFactory;
use App\Entity\Project;
use App\Entity\DatasetFamily;
use App\Entity\Dataset;
use App\Entity\Attribute;
/**
* @author François Agneray <francois.agneray@lam.fr>
......@@ -29,23 +26,6 @@ use App\Entity\Attribute;
*/
final class DatasetListAction extends AbstractAction
{
/**
* @var DBALConnectionFactory
*/
private $connectionFactory;
/**
* Create the classe before call __invoke to execute the action
*
* @param EntityManagerInterface $em Doctrine Entity Manager Interface
* @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database
*/
public function __construct(EntityManagerInterface $em, DBALConnectionFactory $connectionFactory)
{
parent::__construct($em);
$this->connectionFactory = $connectionFactory;
}
/**
* `GET` Returns a list of all datasets for a given dataset family
* `POST` Add a new dataset
......@@ -152,42 +132,8 @@ final class DatasetListAction extends AbstractAction
$dataset->setDatasetFamily($datasetFamily);
$this->em->persist($dataset);
$this->postAttributes($dataset);
$this->em->flush();
return $dataset;
}
/**
* Access to the business database and get all the columns of the table (table_ref)
* and makes the corresponding doctrine attribute objects
*
* @param Dataset $dataset Contains the new dataset doctrine object
*/
private function postAttributes(Dataset $dataset): void
{
$database = $dataset->getProject()->getDatabase();
$connection = $this->connectionFactory->create($database);
$sm = $connection->getSchemaManager();
$columns = $sm->listTableColumns($dataset->getTableRef());
$i = 10;
$id = 1;
foreach ($columns as $column) {
$columnName = $column->getName();
$columnType = $column->getType()->getName();
$attribute = new Attribute($id, $dataset);
$attribute->setName($columnName);
$attribute->setLabel($columnName);
$attribute->setFormLabel($columnName);
$attribute->setType($columnType);
$attribute->setCriteriaDisplay($i);
$attribute->setOutputDisplay($i);
$attribute->setDisplayDetail($i);
$attribute->setOrderDisplay($i);
$attribute->setSelected(true);
$this->em->persist($attribute);
$i = $i + 10;
$id++;
}
}
}
......@@ -42,7 +42,7 @@ final class AttributeActionTest extends TestCase
{
$request = $this->getRequest('OPTIONS');
$response = ($this->action)($request, new Response(), array());
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, OPTIONS');
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS');
}
protected function tearDown(): void
......
......@@ -39,7 +39,7 @@ final class AttributeListActionTest extends TestCase
{
$request = $this->getRequest('OPTIONS');
$response = ($this->action)($request, new Response(), array());
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, OPTIONS');
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS');
}
public function testDatasetIsNotFound(): void
......
......@@ -20,7 +20,10 @@ use Doctrine\ORM\EntityManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\Type;
use App\Entity\Database;
use App\Entity\Project;
use App\Entity\Dataset;
use App\Search\DBALConnectionFactory;
final class ColumnListActionTest extends TestCase
......@@ -47,22 +50,36 @@ final class ColumnListActionTest extends TestCase
{
$this->entityManager->method('find')->willReturn(null);
$this->expectException(HttpNotFoundException::class);
$this->expectExceptionMessage('Database with id 1 is not found');
$this->expectExceptionMessage('Dataset with name test is not found');
$request = $this->getRequest('GET');
$response = ($this->action)($request, new Response(), array('id' => 1));
$response = ($this->action)($request, new Response(), array('name' => 'test'));
$this->assertEquals(404, (int) $response->getStatusCode());
}
public function testGetColumnsList(): void
{
$database = $this->createMock(Database::class);
$this->entityManager->method('find')->willReturn($database);
$project = $this->createMock(Project::class);
$project->method('getDatabase')->willReturn($database);
$dataset = $this->createMock(Dataset::class);
$dataset->method('getProject')->willReturn($project);
$dataset->method('getTableRef')->willReturn('observations');
$this->entityManager->method('find')->willReturn($dataset);
$columnId = $this->createMock(Column::class);
$columnIdType = $this->createMock(Type::class);
$columnIdType->method('getName')->willReturn('integer');
$columnId->method('getName')->willReturn('id');
$columnId->method('getType')->willReturn($columnIdType);
$columnRa = $this->createMock(Column::class);
$columnRaType = $this->createMock(Type::class);
$columnRaType->method('getName')->willReturn('float');
$columnRa->method('getName')->willReturn('ra');
$columnRa->method('getType')->willReturn($columnRaType);
$columnDec = $this->createMock(Column::class);
$columnDecType = $this->createMock(Type::class);
$columnDecType->method('getName')->willReturn('float');
$columnDec->method('getName')->willReturn('dec');
$columnDec->method('getType')->willReturn($columnDecType);
$sm = $this->createMock(AbstractSchemaManager::class);
$sm->method('listTableColumns')->willReturn(array($columnId, $columnRa, $columnDec));
$connection = $this->createMock(Connection::class);
......@@ -70,8 +87,11 @@ final class ColumnListActionTest extends TestCase
$this->connectionFactory->method('create')->willReturn($connection);
$request = $this->getRequest('GET');
$response = ($this->action)($request, new Response(), array('id' => 1, 'tname' => 'observation'));
$this->assertSame(json_encode(array('id', 'ra', 'dec')), (string) $response->getBody());
$response = ($this->action)($request, new Response(), array('name' => 'observations'));
$this->assertSame(
'[{"name":"id","type":"integer"},{"name":"ra","type":"float"},{"name":"dec","type":"float"}]',
(string) $response->getBody()
);
}
private function getRequest(string $method): ServerRequest
......
......@@ -17,11 +17,6 @@ use Nyholm\Psr7\ServerRequest;
use Nyholm\Psr7\Response;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\SqliteSchemaManager;
use Doctrine\DBAL\Schema\Column;
use App\Search\DBALConnectionFactory;
use Doctrine\DBAL\Types\Type;
use App\tests\EntityManagerBuilder;
use App\Entity\Database;
use App\Entity\Project;
......@@ -37,7 +32,7 @@ final class DatasetListActionTest extends TestCase
protected function setUp(): void
{
$this->entityManager = EntityManagerBuilder::getInstance();
$this->action = new \App\Action\DatasetListAction($this->entityManager, $this->getConnectionFactory());
$this->action = new \App\Action\DatasetListAction($this->entityManager);
}
public function testOptionsHttpMethod(): void
......@@ -212,25 +207,4 @@ final class DatasetListActionTest extends TestCase
$this->entityManager->flush();
return array($dataset1, $dataset2);
}
private function getConnectionFactory(): DBALConnectionFactory
{
$schemaManager = $this->createMock(SqliteSchemaManager::class);
$schemaManager->method('listTableColumns')
->will($this->returnValue(array(
new Column('id', Type::getType(Type::INTEGER)),
new Column('ra', Type::getType(Type::FLOAT)),
new Column('dec', Type::getType(TYPE::FLOAT))
)));
$connection = $this->createMock(Connection::class);
$connection->method('getSchemaManager')
->will($this->returnValue($schemaManager));
$connectionFactory = $this->createMock(DBALConnectionFactory::class);
$connectionFactory->method('create')
->will($this->returnValue($connection));
return $connectionFactory;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment