mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-17 15:03:25 +00:00
add:
- multiple pagination methods - "include" parameter in /search route : -- facets : facets are no more returned by default -- results.stories.children : to populate the children of stories - "story_children_limit" parameter in /search route will limit the number of children populated by story - "count" (for results) and "children_count" (for stories in results) is the number of items in the current page - "total" (for results) and "children_total" (for stories in results) is the total number of items todo: swagger doc (but impossible to describe related/excluded parameters (like pagination methods in /search)
This commit is contained in:
@@ -32,6 +32,27 @@ components:
|
|||||||
flows:
|
flows:
|
||||||
password: # <-- OAuth flow(authorizationCode, implicit, password or clientCredentials)
|
password: # <-- OAuth flow(authorizationCode, implicit, password or clientCredentials)
|
||||||
tokenUrl: azea
|
tokenUrl: azea
|
||||||
|
parameters:
|
||||||
|
offsetParam:
|
||||||
|
name: offset
|
||||||
|
in: query
|
||||||
|
description: offset in records count
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
limitParam:
|
||||||
|
name: limit
|
||||||
|
in: query
|
||||||
|
description: number of results
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
default: 10
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
'/me':
|
'/me':
|
||||||
get:
|
get:
|
||||||
@@ -44,6 +65,96 @@ paths:
|
|||||||
default:
|
default:
|
||||||
description: Any error
|
description: Any error
|
||||||
|
|
||||||
|
# ---------------- search ----------------
|
||||||
|
'/search':
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- search
|
||||||
|
description: 'Fulltext search for records or stories.
|
||||||
|
pagination: use (offset/limit) OR (page/per_page)'
|
||||||
|
parameters:
|
||||||
|
- name: query
|
||||||
|
in: query
|
||||||
|
description: 'The fulltext query (<empty> = search all)'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 'dogs OR cats'
|
||||||
|
default: ''
|
||||||
|
- name: search_type
|
||||||
|
in: query
|
||||||
|
description: 0 to search for records ; 1 to search for stories
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 1
|
||||||
|
default: 0
|
||||||
|
- name: offset
|
||||||
|
in: query
|
||||||
|
description: offset in records count, from 0. Use along with "limit"
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
description: number of results. Use along with "offset"
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
default: 10
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
description: page number, from 1. Use along with "per_page"
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
default: 1
|
||||||
|
- name: per_page
|
||||||
|
in: query
|
||||||
|
description: number of results per page. Use along with "page"
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
default: 10
|
||||||
|
- name: story_children_limit
|
||||||
|
in: query
|
||||||
|
description: For stories, include N children
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 10
|
||||||
|
default: 0
|
||||||
|
- name: include
|
||||||
|
in: query
|
||||||
|
description: Elements to be included in response
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
|
||||||
|
anyOf:
|
||||||
|
- enum:
|
||||||
|
- facets
|
||||||
|
- result.stories.children
|
||||||
|
explode: true
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: 'schemas.yaml#/ApiResponse_search'
|
||||||
|
default:
|
||||||
|
$ref: 'responses.yaml#/error_response'
|
||||||
|
|
||||||
|
# ------------ record -----------
|
||||||
'/records/{base_id}':
|
'/records/{base_id}':
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
@@ -63,6 +63,43 @@ ApiResponse_record:
|
|||||||
$ref: '#/ApiResponse_meta'
|
$ref: '#/ApiResponse_meta'
|
||||||
response:
|
response:
|
||||||
$ref: '#/Record'
|
$ref: '#/Record'
|
||||||
|
|
||||||
|
# -------------------- search ---------------
|
||||||
|
ApiResponse_search:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
$ref: '#/ApiResponse_meta'
|
||||||
|
response:
|
||||||
|
$ref: '#/ApiResponse_search_response'
|
||||||
|
ApiResponse_search_response:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
offset:
|
||||||
|
type: integer
|
||||||
|
description: 'The pagination offset as passed in request'
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
description: 'The pagination limit as passed in request'
|
||||||
|
page_result_count:
|
||||||
|
type: integer
|
||||||
|
description: 'The number of results in this page [0...limit]'
|
||||||
|
total_result_count:
|
||||||
|
type: integer
|
||||||
|
description: 'The total number of results request'
|
||||||
|
results:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
stories:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/Record'
|
||||||
|
records:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/Record'
|
||||||
|
|
||||||
|
|
||||||
ID:
|
ID:
|
||||||
type: integer
|
type: integer
|
||||||
Permalink:
|
Permalink:
|
||||||
|
@@ -178,9 +178,9 @@ class V3ResultHelpers
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if($record->isStory()) {
|
if($record->isStory()) {
|
||||||
//
|
$data['children_total'] = $record->getChildrenCount();
|
||||||
// }
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
@@ -261,6 +261,55 @@ class V3ResultHelpers
|
|||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @return int[] [offset, limit]
|
||||||
|
*/
|
||||||
|
public static function paginationFromRequest(Request $request)
|
||||||
|
{
|
||||||
|
// we can deal with "page / per_page" OR "offset / limit" OR "cursor / limit"
|
||||||
|
//
|
||||||
|
$method = '';
|
||||||
|
foreach(['page', 'per_page', 'offset', 'limit', 'cursor'] as $v) {
|
||||||
|
if($request->get($v) !== null) {
|
||||||
|
$method .= ($method?'+':'') . $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$offset = 0; // default
|
||||||
|
$limit = 10; // default
|
||||||
|
switch($method) {
|
||||||
|
case '': // no parms -> default
|
||||||
|
break;
|
||||||
|
case 'page':
|
||||||
|
case 'per_page':
|
||||||
|
case 'page+per_page':
|
||||||
|
$limit = (int)($request->get('per_page') ?: 10);
|
||||||
|
$offset = ((int)($request->get('page') ?: 1) - 1) * $limit; // page starts at 1
|
||||||
|
break;
|
||||||
|
case 'offset':
|
||||||
|
case 'limit':
|
||||||
|
case 'offset+limit':
|
||||||
|
$offset = (int)($request->get('offset') ?: 0);
|
||||||
|
$limit = (int)($request->get('limit') ?: 10);
|
||||||
|
break;
|
||||||
|
case 'cursor':
|
||||||
|
case 'cursor+limit':
|
||||||
|
if( ($cursor = $request->get('cursor')) !== null) {
|
||||||
|
$offset = (int)(base64_decode($cursor));
|
||||||
|
}
|
||||||
|
$limit = (int)($request->get('limit') ?: 10);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// any other combination is invalid
|
||||||
|
throw new \InvalidArgumentException(sprintf('bad pagination "%s" method', $method));
|
||||||
|
}
|
||||||
|
if($offset < 0 || $limit < 1 || $limit > 100) {
|
||||||
|
throw new \InvalidArgumentException("offset must be > 0 ; limit must be [1...100]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return([$offset, $limit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////
|
////////////////////////
|
||||||
private function getAuthenticator()
|
private function getAuthenticator()
|
||||||
|
@@ -8,6 +8,7 @@ use Alchemy\Phrasea\Collection\Reference\CollectionReference;
|
|||||||
use Alchemy\Phrasea\Controller\Api\Result;
|
use Alchemy\Phrasea\Controller\Api\Result;
|
||||||
use Alchemy\Phrasea\Controller\Controller;
|
use Alchemy\Phrasea\Controller\Controller;
|
||||||
use Alchemy\Phrasea\Databox\DataboxGroupable;
|
use Alchemy\Phrasea\Databox\DataboxGroupable;
|
||||||
|
use Alchemy\Phrasea\Databox\Record\LegacyRecordRepository;
|
||||||
use Alchemy\Phrasea\Fractal\CallbackTransformer;
|
use Alchemy\Phrasea\Fractal\CallbackTransformer;
|
||||||
use Alchemy\Phrasea\Fractal\IncludeResolver;
|
use Alchemy\Phrasea\Fractal\IncludeResolver;
|
||||||
use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver;
|
use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver;
|
||||||
@@ -22,14 +23,14 @@ use Alchemy\Phrasea\Search\PermalinkView;
|
|||||||
use Alchemy\Phrasea\Search\RecordTransformer;
|
use Alchemy\Phrasea\Search\RecordTransformer;
|
||||||
use Alchemy\Phrasea\Search\RecordView;
|
use Alchemy\Phrasea\Search\RecordView;
|
||||||
use Alchemy\Phrasea\Search\SearchResultView;
|
use Alchemy\Phrasea\Search\SearchResultView;
|
||||||
use Alchemy\Phrasea\Search\StoryTransformer;
|
|
||||||
use Alchemy\Phrasea\Search\StoryView;
|
use Alchemy\Phrasea\Search\StoryView;
|
||||||
use Alchemy\Phrasea\Search\SubdefTransformer;
|
use Alchemy\Phrasea\Search\SubdefTransformer;
|
||||||
use Alchemy\Phrasea\Search\SubdefView;
|
use Alchemy\Phrasea\Search\SubdefView;
|
||||||
use Alchemy\Phrasea\Search\TechnicalDataTransformer;
|
use Alchemy\Phrasea\Search\TechnicalDataTransformer;
|
||||||
use Alchemy\Phrasea\Search\TechnicalDataView;
|
use Alchemy\Phrasea\Search\TechnicalDataView;
|
||||||
use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer;
|
use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer;
|
||||||
use Alchemy\Phrasea\Search\V1SearchResultTransformer;
|
use Alchemy\Phrasea\Search\V3SearchResultTransformer;
|
||||||
|
use Alchemy\Phrasea\Search\V3StoryTransformer;
|
||||||
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
|
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
|
||||||
use Alchemy\Phrasea\SearchEngine\SearchEngineLogger;
|
use Alchemy\Phrasea\SearchEngine\SearchEngineLogger;
|
||||||
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
|
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
|
||||||
@@ -37,6 +38,7 @@ use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
|
|||||||
use caption_record;
|
use caption_record;
|
||||||
use League\Fractal\Manager as FractalManager;
|
use League\Fractal\Manager as FractalManager;
|
||||||
use League\Fractal\Resource\Item;
|
use League\Fractal\Resource\Item;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
use media_Permalink_Adapter;
|
use media_Permalink_Adapter;
|
||||||
use media_subdef;
|
use media_subdef;
|
||||||
use record_adapter;
|
use record_adapter;
|
||||||
@@ -60,24 +62,25 @@ class V3SearchController extends Controller
|
|||||||
$subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer());
|
$subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer());
|
||||||
$technicalDataTransformer = new TechnicalDataTransformer();
|
$technicalDataTransformer = new TechnicalDataTransformer();
|
||||||
$recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer);
|
$recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer);
|
||||||
$storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer);
|
$storyTransformer = new V3StoryTransformer($subdefTransformer, $recordTransformer);
|
||||||
$compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer);
|
$compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer);
|
||||||
$searchTransformer = new V1SearchResultTransformer($compositeTransformer);
|
$searchTransformer = new V3SearchResultTransformer($compositeTransformer);
|
||||||
|
|
||||||
$transformerResolver = new SearchResultTransformerResolver([
|
$transformerResolver = new SearchResultTransformerResolver([
|
||||||
'' => $searchTransformer,
|
'' => $searchTransformer,
|
||||||
'results' => $compositeTransformer,
|
'results' => $compositeTransformer,
|
||||||
|
'facets' => new CallbackTransformer(),
|
||||||
'results.stories' => $storyTransformer,
|
'results.stories' => $storyTransformer,
|
||||||
'results.stories.thumbnail' => $subdefTransformer,
|
'results.stories.thumbnail' => $subdefTransformer,
|
||||||
'results.stories.metadatas' => new CallbackTransformer(),
|
'results.stories.metadatas' => new CallbackTransformer(),
|
||||||
'results.stories.caption' => new CallbackTransformer(),
|
'results.stories.caption' => new CallbackTransformer(),
|
||||||
'results.stories.records' => $recordTransformer,
|
'results.stories.children' => $recordTransformer,
|
||||||
'results.stories.records.thumbnail' => $subdefTransformer,
|
'results.stories.children.thumbnail' => $subdefTransformer,
|
||||||
'results.stories.records.technical_informations' => $technicalDataTransformer,
|
'results.stories.children.technical_informations' => $technicalDataTransformer,
|
||||||
'results.stories.records.subdefs' => $subdefTransformer,
|
'results.stories.children.subdefs' => $subdefTransformer,
|
||||||
'results.stories.records.metadata' => new CallbackTransformer(),
|
'results.stories.children.metadata' => new CallbackTransformer(),
|
||||||
'results.stories.records.status' => new CallbackTransformer(),
|
'results.stories.children.status' => new CallbackTransformer(),
|
||||||
'results.stories.records.caption' => new CallbackTransformer(),
|
'results.stories.children.caption' => new CallbackTransformer(),
|
||||||
'results.records' => $recordTransformer,
|
'results.records' => $recordTransformer,
|
||||||
'results.records.thumbnail' => $subdefTransformer,
|
'results.records.thumbnail' => $subdefTransformer,
|
||||||
'results.records.technical_informations' => $technicalDataTransformer,
|
'results.records.technical_informations' => $technicalDataTransformer,
|
||||||
@@ -91,21 +94,23 @@ class V3SearchController extends Controller
|
|||||||
|
|
||||||
$fractal = new FractalManager();
|
$fractal = new FractalManager();
|
||||||
$fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher']));
|
$fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher']));
|
||||||
$fractal->parseIncludes($this->resolveSearchIncludes($request));
|
|
||||||
|
// and push everything back to fractal
|
||||||
|
$fractal->parseIncludes($this->getIncludes($request));
|
||||||
|
|
||||||
$result = $this->doSearch($request);
|
$result = $this->doSearch($request);
|
||||||
|
|
||||||
$story_max_records = null;
|
$story_children_limit = null;
|
||||||
// if search on story
|
// if searching stories
|
||||||
if ($request->get('search_type') == 1) {
|
if ($request->get('search_type') == 1) {
|
||||||
$story_max_records = (int)$request->get('story_max_records') ?: 10;
|
$story_children_limit = (int)$request->get('story_children_limit') ?: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
$searchView = $this->buildSearchView(
|
$searchView = $this->buildSearchView(
|
||||||
$result,
|
$result,
|
||||||
$includeResolver->resolve($fractal),
|
$includeResolver->resolve($fractal),
|
||||||
$this->resolveSubdefUrlTTL($request),
|
$this->resolveSubdefUrlTTL($request),
|
||||||
$story_max_records
|
$story_children_limit
|
||||||
);
|
);
|
||||||
|
|
||||||
$ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray();
|
$ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray();
|
||||||
@@ -113,25 +118,30 @@ class V3SearchController extends Controller
|
|||||||
return Result::create($request, $ret)->createResponse();
|
return Result::create($request, $ret)->createResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns requested includes
|
* Returns requested includes
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
private function resolveSearchIncludes(Request $request)
|
private function getIncludes(Request $request)
|
||||||
{
|
{
|
||||||
$includes = [
|
// a local fractal manager will help to smartly parse the request parameters.
|
||||||
'results.stories.records'
|
$fractal = new FractalManager();
|
||||||
];
|
|
||||||
|
// first, get includes from request
|
||||||
|
//
|
||||||
|
$fractal->parseIncludes($request->get('include', []));
|
||||||
|
$includes = $fractal->getRequestedIncludes();
|
||||||
|
|
||||||
if ($request->attributes->get('_extended', false)) {
|
if ($request->attributes->get('_extended', false)) {
|
||||||
if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) {
|
// if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) {
|
||||||
|
if(in_array('results.stories.children', $includes)) {
|
||||||
$includes = array_merge($includes, [
|
$includes = array_merge($includes, [
|
||||||
'results.stories.records.subdefs',
|
'results.stories.children.subdefs',
|
||||||
'results.stories.records.metadata',
|
'results.stories.children.metadata',
|
||||||
'results.stories.records.caption',
|
'results.stories.children.caption',
|
||||||
'results.stories.records.status'
|
'results.stories.children.status'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -146,7 +156,13 @@ class V3SearchController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $includes;
|
// push back to fractal (it will keep values uniques)
|
||||||
|
//
|
||||||
|
$fractal->parseIncludes($includes);
|
||||||
|
|
||||||
|
// finally get the result
|
||||||
|
//
|
||||||
|
return $fractal->getRequestedIncludes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,14 +192,26 @@ class V3SearchController extends Controller
|
|||||||
if ($stories->count() > 0) {
|
if ($stories->count() > 0) {
|
||||||
$user = $this->getAuthenticatedUser();
|
$user = $this->getAuthenticatedUser();
|
||||||
$children = [];
|
$children = [];
|
||||||
|
$childrenCounts = [];
|
||||||
|
|
||||||
|
// todo : refacto to remove over-usage of array_map, array_combine, array_flip etc.
|
||||||
|
//
|
||||||
foreach ($stories->getDataboxIds() as $databoxId) {
|
foreach ($stories->getDataboxIds() as $databoxId) {
|
||||||
$storyIds = $stories->getDataboxRecordIds($databoxId);
|
$storyIds = $stories->getDataboxRecordIds($databoxId);
|
||||||
|
|
||||||
$selections = $this->findDataboxById($databoxId)
|
/** @var LegacyRecordRepository $repo */
|
||||||
->getRecordRepository()
|
$repo = $this->findDataboxById($databoxId)->getRecordRepository();
|
||||||
->findChildren($storyIds, $user,1, $story_max_records);
|
|
||||||
$children[$databoxId] = array_combine($storyIds, $selections);
|
// nb : findChildren() and getChildrenCounts() acts on MULTIPLE story-ids in single sql
|
||||||
|
//
|
||||||
|
$childrenCounts[$databoxId] = $repo->getChildrenCounts($storyIds, $user);
|
||||||
|
|
||||||
|
// search children only if needed
|
||||||
|
//
|
||||||
|
if(in_array('results.stories.children', $includes, true)) {
|
||||||
|
$selections = $repo->findChildren($storyIds, $user, 0, $story_max_records);
|
||||||
|
$children[$databoxId] = array_combine($storyIds, $selections);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var StoryView[] $storyViews */
|
/** @var StoryView[] $storyViews */
|
||||||
@@ -194,15 +222,23 @@ class V3SearchController extends Controller
|
|||||||
foreach ($stories as $index => $story) {
|
foreach ($stories as $index => $story) {
|
||||||
$storyView = new StoryView($story);
|
$storyView = new StoryView($story);
|
||||||
|
|
||||||
$selection = $children[$story->getDataboxId()][$story->getRecordId()];
|
// populate children only if needed
|
||||||
|
//
|
||||||
|
if(in_array('results.stories.children', $includes, true)) {
|
||||||
|
$selection = $children[$story->getDataboxId()][$story->getRecordId()];
|
||||||
|
|
||||||
$childrenView = $this->buildRecordViews($selection);
|
$childrenView = $this->buildRecordViews($selection);
|
||||||
|
|
||||||
foreach ($childrenView as $view) {
|
foreach ($childrenView as $view) {
|
||||||
$childrenViews[spl_object_hash($view)] = $view;
|
$childrenViews[spl_object_hash($view)] = $view;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storyView->setChildren($childrenView);
|
||||||
}
|
}
|
||||||
|
|
||||||
$storyView->setChildren($childrenView);
|
$storyView->setData('childrenOffset', 0);
|
||||||
|
$storyView->setData('childrenLimit', $story_max_records);
|
||||||
|
$storyView->setData('childrenCount', $childrenCounts[$story->getDataboxId()][$story->getRecordId()]);
|
||||||
|
|
||||||
$storyViews[$index] = $storyView;
|
$storyViews[$index] = $storyView;
|
||||||
}
|
}
|
||||||
@@ -278,9 +314,12 @@ class V3SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function doSearch(Request $request)
|
private function doSearch(Request $request)
|
||||||
{
|
{
|
||||||
|
list($offset, $limit) = V3ResultHelpers::paginationFromRequest($request);
|
||||||
|
|
||||||
$options = SearchEngineOptions::fromRequest($this->app, $request);
|
$options = SearchEngineOptions::fromRequest($this->app, $request);
|
||||||
$options->setFirstResult((int)($request->get('offset_start') ?: 0));
|
|
||||||
$options->setMaxResults((int)$request->get('per_page') ?: 10);
|
$options->setFirstResult($offset);
|
||||||
|
$options->setMaxResults($limit);
|
||||||
|
|
||||||
$this->getSearchEngine()->resetCache();
|
$this->getSearchEngine()->resetCache();
|
||||||
|
|
||||||
|
@@ -41,11 +41,10 @@ class V3StoriesController extends V3RecordController
|
|||||||
throw new NotFoundHttpException();
|
throw new NotFoundHttpException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$per_page = (int)$request->get('per_page')?:10;
|
list($offset, $limit) = V3ResultHelpers::paginationFromRequest($request);
|
||||||
$page = (int)$request->get('page')?:1;
|
|
||||||
$offset = ($per_page * ($page - 1)) + 1;
|
$ret = $this->listRecords($request, array_values($story->getChildren($offset, $limit)->get_elements()));
|
||||||
|
|
||||||
$ret = $this->listRecords($request, array_values($story->getChildren($offset, $per_page)->get_elements()));
|
|
||||||
return Result::create($request, $ret)->createResponse();
|
return Result::create($request, $ret)->createResponse();
|
||||||
}
|
}
|
||||||
catch (NotFoundHttpException $e) {
|
catch (NotFoundHttpException $e) {
|
||||||
@@ -56,23 +55,6 @@ class V3StoriesController extends V3RecordController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve detailed information about one story
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param record_adapter $story
|
|
||||||
* @return array
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
private function listStory(Request $request, record_adapter $story)
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
$ret = $this->getResultHelpers()->listRecord($request, $story, $this->getAclForUser());
|
|
||||||
$ret['records'] = $this->listRecords($request, array_values($story->getChildren($offset, $per_page)->get_elements()));
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
@@ -185,7 +185,70 @@ class LegacyRecordRepository implements RecordRepository
|
|||||||
return $this->mapRecordsFromResultSet($result);
|
return $this->mapRecordsFromResultSet($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findChildren(array $storyIds, $user = null, $offset = 1, $max_items = null)
|
/**
|
||||||
|
* return the number of VISIBLE children of ONE story, for a specific user
|
||||||
|
* if user is null -> count all children
|
||||||
|
*
|
||||||
|
* @param int $storyId
|
||||||
|
* @param User|int|null $user // can pass a User, or a user_id
|
||||||
|
*
|
||||||
|
* @return int // -1 if story not found
|
||||||
|
*/
|
||||||
|
public function getChildrenCount($storyId, $user = null)
|
||||||
|
{
|
||||||
|
$r = $this->getChildrenCounts([$storyId], $user);
|
||||||
|
|
||||||
|
return $r[$storyId];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the number of VISIBLE children of MANY stories, for a specific user
|
||||||
|
* if user is null -> count all children
|
||||||
|
*
|
||||||
|
* @param int[] $storyIds
|
||||||
|
* @param User|int|null $user // can pass a User, or a user_id
|
||||||
|
*
|
||||||
|
* @return int[] // story_id => n_children (-1 if story not found)
|
||||||
|
*/
|
||||||
|
public function getChildrenCounts(array $storyIds, $user = null)
|
||||||
|
{
|
||||||
|
$connection = $this->databox->get_connection();
|
||||||
|
|
||||||
|
$parmValues = [
|
||||||
|
':storyIds' => $storyIds,
|
||||||
|
];
|
||||||
|
$parmTypes = [
|
||||||
|
':storyIds' => Connection::PARAM_INT_ARRAY,
|
||||||
|
];
|
||||||
|
|
||||||
|
// if there is a user, we must join collusr to filter results depending on coll/masks
|
||||||
|
//
|
||||||
|
$userFilter = "";
|
||||||
|
if(!is_null($user)) {
|
||||||
|
$userFilter = " INNER JOIN collusr c ON c.site = :site AND c.usr_id = :userId AND c.coll_id=r.coll_id AND ((r.status ^ c.mask_xor) & c.mask_and) = 0\n";
|
||||||
|
$parmValues[':site'] = $this->site;
|
||||||
|
$parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT g.rid_parent AS story_id, COUNT(*) AS n_children\n"
|
||||||
|
. " FROM regroup g\n"
|
||||||
|
. " INNER JOIN record r ON r.record_id=g.rid_child\n"
|
||||||
|
. $userFilter
|
||||||
|
. " WHERE g.rid_parent IN( :storyIds )\n"
|
||||||
|
. " GROUP BY g.rid_parent\n"
|
||||||
|
;
|
||||||
|
|
||||||
|
$r = array_fill_keys($storyIds, -1);
|
||||||
|
foreach($connection->fetchAll($sql, $parmValues, $parmTypes) as $row) {
|
||||||
|
$r[$row['story_id']] = (int)$row['n_children'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function findChildren(array $storyIds, $user = null, $offset = 0, $max_items = null)
|
||||||
{
|
{
|
||||||
if (!$storyIds) {
|
if (!$storyIds) {
|
||||||
return [];
|
return [];
|
||||||
@@ -216,7 +279,7 @@ class LegacyRecordRepository implements RecordRepository
|
|||||||
$parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user;
|
$parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($max_items) {
|
if ($max_items !== null) {
|
||||||
//
|
//
|
||||||
// we want paginated results AFTER applying all filters, we build a dynamic cptr
|
// we want paginated results AFTER applying all filters, we build a dynamic cptr
|
||||||
// WARNING : due to bugs (?) in mysql optimizer, do NOT try to optimize this sql (e.g. removing a sub-q, or moving cpt to anothe sub-q)
|
// WARNING : due to bugs (?) in mysql optimizer, do NOT try to optimize this sql (e.g. removing a sub-q, or moving cpt to anothe sub-q)
|
||||||
@@ -235,11 +298,11 @@ class LegacyRecordRepository implements RecordRepository
|
|||||||
. " ORDER BY g.rid_parent, g.ord ASC\n"
|
. " ORDER BY g.rid_parent, g.ord ASC\n"
|
||||||
. " ) t\n"
|
. " ) t\n"
|
||||||
. ") r\n"
|
. ") r\n"
|
||||||
. "WHERE CPT BETWEEN :offset AND :maxresult"
|
. "WHERE CPT BETWEEN :cptmin AND :cptmax"
|
||||||
;
|
;
|
||||||
|
|
||||||
$parmValues[':offset'] = $offset;
|
$parmValues[':cptmin'] = $offset + 1;
|
||||||
$parmValues[':maxresult'] = ($offset + $max_items -1);
|
$parmValues[':cptmax'] = $offset + $max_items;
|
||||||
|
|
||||||
$connection->executeQuery('SET @cpt = 1');
|
$connection->executeQuery('SET @cpt = 1');
|
||||||
$connection->executeQuery('SET @old_rid_parent = -1');
|
$connection->executeQuery('SET @old_rid_parent = -1');
|
||||||
@@ -264,6 +327,8 @@ class LegacyRecordRepository implements RecordRepository
|
|||||||
|
|
||||||
$records = $this->mapRecordsFromResultSet($data);
|
$records = $this->mapRecordsFromResultSet($data);
|
||||||
|
|
||||||
|
// todo : refacto to remove over-usage of array_map, array_combine, array_flip etc.
|
||||||
|
//
|
||||||
$selections = array_map(
|
$selections = array_map(
|
||||||
function () {
|
function () {
|
||||||
return new set_selection($this->app);
|
return new set_selection($this->app);
|
||||||
|
@@ -61,7 +61,7 @@ interface RecordRepository
|
|||||||
* @param null|int $max_items
|
* @param null|int $max_items
|
||||||
* @return \set_selection[]
|
* @return \set_selection[]
|
||||||
*/
|
*/
|
||||||
public function findChildren(array $storyIds, $user = null, $offset = 1, $max_items = null);
|
public function findChildren(array $storyIds, $user = null, $offset = 0, $max_items = null);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -12,6 +12,17 @@ namespace Alchemy\Phrasea\Search;
|
|||||||
|
|
||||||
use Assert\Assertion;
|
use Assert\Assertion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used as a (temporary / specific) view of a story
|
||||||
|
* WARNING : the children DO NOT NECESSARILY reflect the whole content of a story
|
||||||
|
* children may contain only a subset :
|
||||||
|
* - visible for a specific user
|
||||||
|
* - paginated
|
||||||
|
*
|
||||||
|
* Class StoryView
|
||||||
|
* @package Alchemy\Phrasea\Search
|
||||||
|
*/
|
||||||
|
|
||||||
class StoryView
|
class StoryView
|
||||||
{
|
{
|
||||||
use SubdefsAware;
|
use SubdefsAware;
|
||||||
@@ -21,11 +32,20 @@ class StoryView
|
|||||||
* @var \record_adapter
|
* @var \record_adapter
|
||||||
*/
|
*/
|
||||||
private $story;
|
private $story;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var RecordView[]
|
* @var RecordView[]
|
||||||
|
* may be a subset of all children (only visibles for a user and/or paginated)
|
||||||
*/
|
*/
|
||||||
private $children = [];
|
private $children = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed[]
|
||||||
|
* "personal use" data to be stored/retreived for a specific usage
|
||||||
|
* e.g. api V3 will store pagination infos to be rendered on output
|
||||||
|
*/
|
||||||
|
private $_data = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \record_adapter $story
|
* @param \record_adapter $story
|
||||||
*/
|
*/
|
||||||
@@ -44,12 +64,15 @@ class StoryView
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param RecordView[] $children
|
* @param RecordView[] $children
|
||||||
|
* @return self
|
||||||
*/
|
*/
|
||||||
public function setChildren($children)
|
public function setChildren($children)
|
||||||
{
|
{
|
||||||
Assertion::allIsInstanceOf($children, RecordView::class);
|
Assertion::allIsInstanceOf($children, RecordView::class);
|
||||||
|
|
||||||
$this->children = $children instanceof \Traversable ? iterator_to_array($children, false) : array_values($children);
|
$this->children = $children instanceof \Traversable ? iterator_to_array($children, false) : array_values($children);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,4 +82,29 @@ class StoryView
|
|||||||
{
|
{
|
||||||
return $this->children;
|
return $this->children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set a "personal usage" data
|
||||||
|
*
|
||||||
|
* @param string $k
|
||||||
|
* @param mixed $v
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setData($k, $v)
|
||||||
|
{
|
||||||
|
$this->_data[$k] = $v;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a "personal usage" data (null if not found)
|
||||||
|
*
|
||||||
|
* @param string $k
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getData($k)
|
||||||
|
{
|
||||||
|
return isset($this->_data[$k]) ? $this->_data[$k] : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
59
lib/Alchemy/Phrasea/Search/V3SearchResultTransformer.php
Normal file
59
lib/Alchemy/Phrasea/Search/V3SearchResultTransformer.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Phraseanet
|
||||||
|
*
|
||||||
|
* (c) 2005-2016 Alchemy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alchemy\Phrasea\Search;
|
||||||
|
|
||||||
|
use League\Fractal\TransformerAbstract;
|
||||||
|
|
||||||
|
class V3SearchResultTransformer extends TransformerAbstract
|
||||||
|
{
|
||||||
|
protected $availableIncludes = ['results', 'facets'];
|
||||||
|
protected $defaultIncludes = ['results'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TransformerAbstract
|
||||||
|
*/
|
||||||
|
private $transformer;
|
||||||
|
|
||||||
|
public function __construct(TransformerAbstract $transformer)
|
||||||
|
{
|
||||||
|
$this->transformer = $transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform(SearchResultView $resultView)
|
||||||
|
{
|
||||||
|
$result = $resultView->getResult();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'query' => $result->getQueryText(),
|
||||||
|
'offset' => $result->getOptions()->getFirstResult(),
|
||||||
|
'limit' => $result->getOptions()->getMaxResults(),
|
||||||
|
'count' => $result->getAvailable(),
|
||||||
|
'total' => $result->getTotal(),
|
||||||
|
'error' => (string)$result->getError(),
|
||||||
|
'warning' => (string)$result->getWarning(),
|
||||||
|
'query_time' => $result->getDuration(),
|
||||||
|
'search_indexes' => $result->getIndexes(),
|
||||||
|
// 'facets' => $result->getFacets(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includeResults(SearchResultView $resultView)
|
||||||
|
{
|
||||||
|
return $this->item($resultView, $this->transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includeFacets(SearchResultView $resultView)
|
||||||
|
{
|
||||||
|
return $this->item($resultView->getResult()->getFacets(), function ($x) {
|
||||||
|
return $x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
67
lib/Alchemy/Phrasea/Search/V3StoryTransformer.php
Normal file
67
lib/Alchemy/Phrasea/Search/V3StoryTransformer.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Phraseanet
|
||||||
|
*
|
||||||
|
* (c) 2005-2016 Alchemy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Alchemy\Phrasea\Search;
|
||||||
|
|
||||||
|
use Alchemy\Phrasea\Utilities\NullableDateTime;
|
||||||
|
|
||||||
|
class V3StoryTransformer extends StoryTransformer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RecordTransformer
|
||||||
|
*/
|
||||||
|
private $recordTransformer;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $availableIncludes = ['thumbnail', 'metadatas', 'children', 'caption'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $defaultIncludes = ['thumbnail', 'metadatas'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SubdefTransformer $subdefTransformer
|
||||||
|
* @param RecordTransformer $recordTransformer
|
||||||
|
*/
|
||||||
|
public function __construct(SubdefTransformer $subdefTransformer, RecordTransformer $recordTransformer)
|
||||||
|
{
|
||||||
|
parent::__construct($subdefTransformer, $recordTransformer);
|
||||||
|
$this->recordTransformer = $recordTransformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform(StoryView $storyView)
|
||||||
|
{
|
||||||
|
$story = $storyView->getStory();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'databox_id' => $story->getDataboxId(),
|
||||||
|
'story_id' => $story->getRecordId(),
|
||||||
|
'updated_on' => NullableDateTime::format($story->getUpdated()),
|
||||||
|
'created_on' => NullableDateTime::format($story->getUpdated()),
|
||||||
|
'collection_id' => $story->getCollectionId(),
|
||||||
|
'base_id' => $story->getBaseId(),
|
||||||
|
'uuid' => $story->getUuid(),
|
||||||
|
'children_offset' => $storyView->getData('childrenOffset'),
|
||||||
|
'children_limit' => $storyView->getData('childrenLimit'),
|
||||||
|
'children_count' => count($storyView->getChildren()),
|
||||||
|
'children_total' => $storyView->getData('childrenCount') // fix V1 wrong count
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includeChildren(StoryView $storyView)
|
||||||
|
{
|
||||||
|
return $this->collection($storyView->getChildren(), $this->recordTransformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -320,7 +320,7 @@ class ElasticSearchEngine implements SearchEngineInterface
|
|||||||
$queryESLib,
|
$queryESLib,
|
||||||
$res['took'], // duration
|
$res['took'], // duration
|
||||||
$options->getFirstResult(),
|
$options->getFirstResult(),
|
||||||
$res['hits']['total'], // available
|
count($res['hits']['hits']), // available
|
||||||
$res['hits']['total'], // total
|
$res['hits']['total'], // total
|
||||||
null, // error
|
null, // error
|
||||||
null, // warning
|
null, // warning
|
||||||
|
@@ -1784,7 +1784,7 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
* @throws DBALException
|
* @throws DBALException
|
||||||
*/
|
*/
|
||||||
public function getChildren($offset = 1, $max_items = null)
|
public function getChildren($offset = 0, $max_items = null)
|
||||||
{
|
{
|
||||||
if (!$this->isStory()) {
|
if (!$this->isStory()) {
|
||||||
throw new Exception('This record is not a grouping');
|
throw new Exception('This record is not a grouping');
|
||||||
@@ -1797,6 +1797,17 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
|
|||||||
return reset($selections);
|
return reset($selections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getChildrenCount()
|
||||||
|
{
|
||||||
|
if (!$this->isStory()) {
|
||||||
|
throw new Exception('This record is not a grouping');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->getAuthenticatedUser();
|
||||||
|
|
||||||
|
return $this->getDatabox()->getRecordRepository()->getChildrenCount($this->getRecordId(), $user);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return set_selection
|
* @return set_selection
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user