diff --git a/config/configuration.sample.yml b/config/configuration.sample.yml index fbaa368ed9..b7fef2618a 100644 --- a/config/configuration.sample.yml +++ b/config/configuration.sample.yml @@ -28,7 +28,8 @@ main: port: 11211 search-engine: type: phrasea - options: [] + options: + facets: [] task-manager: status: started enabled: true diff --git a/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php b/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php index 8cf04e7839..04762b3aef 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php @@ -13,9 +13,11 @@ namespace Alchemy\Phrasea\Controller\Admin; use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchSettingsFormType; use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions; +use Alchemy\Phrasea\SearchEngine\Elastic\Structure\GlobalStructure; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use databox_descriptionStructure; class SearchEngineController extends Controller { @@ -31,7 +33,19 @@ class SearchEngineController extends Controller $form->handleRequest($request); if ($form->isValid()) { - $this->saveElasticSearchOptions($form->getData()); + /** @var ElasticsearchOptions $data */ + $data = $form->getData(); + // $q = $request->request->get('elasticsearch_settings'); + $facetNames = []; // rebuild the data "_customValues/facets" list following the form order + foreach($request->request->get('elasticsearch_settings') as $name=>$value) { + $matches = null; + if(preg_match('/^facets:(.+):limit$/', $name, $matches) === 1) { + $facetNames[] = $matches[1]; + } + } + $data->reorderAggregableFields($facetNames); + + $this->saveElasticSearchOptions($data); return $this->app->redirectPath('admin_searchengine_form'); } @@ -76,6 +90,16 @@ class SearchEngineController extends Controller */ private function saveElasticSearchOptions(ElasticsearchOptions $configuration) { + // save to databoxes fields for backward compatibility (useless ?) + foreach($configuration->getAggregableFields() as $fname=>$aggregableField) { + foreach ($this->app->getDataboxes() as $databox) { + if(!is_null($f = $databox->get_meta_structure()->get_element_by_name($fname, databox_descriptionStructure::STRICT_COMPARE))) { + $f->set_aggregable($aggregableField['limit'])->save(); + } + } + } + + // save to conf $this->getConf()->set(['main', 'search-engine', 'options'], $configuration->toArray()); } @@ -85,7 +109,10 @@ class SearchEngineController extends Controller */ private function getConfigurationForm(ElasticsearchOptions $options) { - return $this->app->form(new ElasticsearchSettingsFormType(), $options, [ + /** @var GlobalStructure $g */ + $g = $this->app['search_engine.structure']; + + return $this->app->form(new ElasticsearchSettingsFormType($g, $options), $options, [ 'action' => $this->app->url('admin_searchengine_form'), ]); } diff --git a/lib/Alchemy/Phrasea/Controller/Prod/FeedController.php b/lib/Alchemy/Phrasea/Controller/Prod/FeedController.php index 2bc333f45c..39a0a8f48d 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/FeedController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/FeedController.php @@ -19,8 +19,10 @@ use Alchemy\Phrasea\Core\PhraseaEvents; use Alchemy\Phrasea\Feed\Aggregate; use Alchemy\Phrasea\Feed\Link\AggregateLinkGenerator; use Alchemy\Phrasea\Feed\Link\FeedLinkGenerator; +use Alchemy\Phrasea\Model\Entities\Feed; use Alchemy\Phrasea\Model\Entities\FeedEntry; use Alchemy\Phrasea\Model\Entities\FeedItem; +use Alchemy\Phrasea\Model\Entities\FeedPublisher; use Alchemy\Phrasea\Model\Repositories\FeedEntryRepository; use Alchemy\Phrasea\Model\Repositories\FeedItemRepository; use Alchemy\Phrasea\Model\Repositories\FeedPublisherRepository; @@ -46,6 +48,7 @@ class FeedController extends Controller } public function createFeedEntryAction(Request $request) { + /** @var Feed $feed */ $feed = $this->getFeedRepository()->find($request->request->get('feed_id')); if (null === $feed) { @@ -53,6 +56,8 @@ class FeedController extends Controller } $user = $this->getAuthenticatedUser(); + + /** @var FeedPublisher $publisher */ $publisher = $this->getFeedPublisherRepository()->findOneBy([ 'feed' => $feed, 'user' => $user, diff --git a/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php b/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php index d327e860c5..8afd22d2b3 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php @@ -433,24 +433,15 @@ class QueryController extends Controller // populates facets (aggregates) $facets = []; - // $facetClauses = []; foreach ($result->getFacets() as $facet) { $facetName = $facet['name']; if(array_key_exists($facetName, $fieldsInfosByName)) { - $f = $fieldsInfosByName[$facetName]; - $facet['label'] = $f['trans_label']; $facet['labels'] = $f['labels']; $facet['type'] = strtoupper($f['type']) . "-AGGREGATE"; $facets[] = $facet; - - // $facetClauses[] = [ - // 'type' => strtoupper($f['type']) . "-AGGREGATE", - // 'field' => $f['field'], - // 'facet' => $facet - // ]; } } diff --git a/lib/Alchemy/Phrasea/Core/Event/Subscriber/FeedEntrySubscriber.php b/lib/Alchemy/Phrasea/Core/Event/Subscriber/FeedEntrySubscriber.php index 03a72a95b4..8bc7cb2b52 100644 --- a/lib/Alchemy/Phrasea/Core/Event/Subscriber/FeedEntrySubscriber.php +++ b/lib/Alchemy/Phrasea/Core/Event/Subscriber/FeedEntrySubscriber.php @@ -13,7 +13,9 @@ namespace Alchemy\Phrasea\Core\Event\Subscriber; use Alchemy\Phrasea\Core\Event\FeedEntryEvent; use Alchemy\Phrasea\Core\PhraseaEvents; +use Alchemy\Phrasea\Model\Entities\User; use Alchemy\Phrasea\Model\Entities\WebhookEvent; +use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Alchemy\Phrasea\Notification\Receiver; use Alchemy\Phrasea\Notification\Mail\MailInfoNewPublication; @@ -53,36 +55,43 @@ class FeedEntrySubscriber extends AbstractNotificationSubscriber do { $results = $Query->limit($start, $perLoop)->execute()->get_results(); - foreach ($results as $user_to_notif) { - $mailed = false; + $users_emailed = []; // for all users + $users_to_email = []; // list only users who must be emailed (=create tokens) - if ($params['notify_email'] && $this->shouldSendNotificationFor($user_to_notif, 'eventsmanager_notify_feed')) { - $readyToSend = false; - try { - $token = $this->app['manipulator.token']->createFeedEntryToken($user_to_notif, $entry); - $url = $this->app->url('lightbox', ['LOG' => $token->getValue()]); - - $receiver = Receiver::fromUser($user_to_notif); - $readyToSend = true; - } catch (\Exception $e) { - - } - - if ($readyToSend) { - $mail = MailInfoNewPublication::create($this->app, $receiver); - $mail->setButtonUrl($url); - $mail->setAuthor($entry->getAuthorName()); - $mail->setTitle($entry->getTitle()); - - $this->deliver($mail); - $mailed = true; - } + /** @var User $user */ + foreach ($results as $user) { + $users_emailed[$user->getId()] = false; + if ($params['notify_email'] && $this->shouldSendNotificationFor($user, 'eventsmanager_notify_feed')) { + $users_to_email[$user->getId()] = $user; } - - $this->app['events-manager']->notify($user_to_notif->getId(), 'eventsmanager_notify_feed', $datas, $mailed); } + + // get many tokens in one shot + $tokens = $this->getTokenManipulator()->createFeedEntryTokens($users_to_email, $entry); + foreach($tokens as $token) { + try { + $url = $this->app->url('lightbox', ['LOG' => $token->getValue()]); + $receiver = Receiver::fromUser($token->getUser()); + + $mail = MailInfoNewPublication::create($this->app, $receiver); + $mail->setButtonUrl($url); + $mail->setAuthor($entry->getAuthorName()); + $mail->setTitle($entry->getTitle()); + + $this->deliver($mail); + $users_emailed[$token->getUser()->getId()] = true; + } + catch (\Exception $e) { + // no-op + } + } + foreach($users_emailed as $id => $emailed) { + $this->app['events-manager']->notify($id, 'eventsmanager_notify_feed', $datas, $emailed); + } + $start += $perLoop; - } while (count($results) > 0); + } + while (count($results) > 0); } public static function getSubscribedEvents() @@ -91,4 +100,12 @@ class FeedEntrySubscriber extends AbstractNotificationSubscriber PhraseaEvents::FEED_ENTRY_CREATE => 'onCreate', ]; } + + /** + * @return TokenManipulator + */ + private function getTokenManipulator() + { + return $this->app['manipulator.token']; + } } diff --git a/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php index 58f77475a1..e342bf8380 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php @@ -97,7 +97,7 @@ class SearchEngineServiceProvider implements ServiceProviderInterface }); $app['elasticsearch.facets_response.factory'] = $app->protect(function (array $response) use ($app) { - return new FacetsResponse(new Escaper(), $response, $app['search_engine.structure']); + return new FacetsResponse($app['elasticsearch.options'], new Escaper(), $response, $app['search_engine.structure']); }); return $app; @@ -228,7 +228,8 @@ class SearchEngineServiceProvider implements ServiceProviderInterface }); $app['elasticsearch.options'] = $app->share(function ($app) { - $options = ElasticsearchOptions::fromArray($app['conf']->get(['main', 'search-engine', 'options'], [])); + $conf = $app['conf']->get(['main', 'search-engine', 'options'], []); + $options = ElasticsearchOptions::fromArray($conf); if (empty($options->getIndexName())) { $options->setIndexName(strtolower(sprintf('phraseanet_%s', str_replace( diff --git a/lib/Alchemy/Phrasea/Core/Version.php b/lib/Alchemy/Phrasea/Core/Version.php index 5f488cdd5a..d17062de4c 100644 --- a/lib/Alchemy/Phrasea/Core/Version.php +++ b/lib/Alchemy/Phrasea/Core/Version.php @@ -16,7 +16,7 @@ class Version /** * @var string */ - private $number = '4.1.0-alpha.20a'; + private $number = '4.1.0-alpha.22a'; /** * @var string diff --git a/lib/Alchemy/Phrasea/Model/Entities/Token.php b/lib/Alchemy/Phrasea/Model/Entities/Token.php index 576cf05d36..81c632f97d 100644 --- a/lib/Alchemy/Phrasea/Model/Entities/Token.php +++ b/lib/Alchemy/Phrasea/Model/Entities/Token.php @@ -15,7 +15,14 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** - * @ORM\Table(name="Tokens") + * @ORM\Table(name="Tokens", + * indexes={ + * @ORM\index(name="type", columns={"type"}), + * @ORM\index(name="created", columns={"created"}), + * @ORM\index(name="updated", columns={"updated"}), + * @ORM\index(name="expiration", columns={"expiration"}) + * } + * ) * @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\TokenRepository") */ class Token diff --git a/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php b/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php index 2a43363693..56c4e9fc36 100644 --- a/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php +++ b/lib/Alchemy/Phrasea/Model/Manipulator/TokenManipulator.php @@ -22,7 +22,6 @@ use RandomLib\Generator; class TokenManipulator implements ManipulatorInterface { const LETTERS_AND_NUMBERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const TYPE_FEED_ENTRY = 'FEED_ENTRY'; const TYPE_PASSWORD = 'password'; const TYPE_ACCOUNT_UNLOCK = 'account-unlock'; @@ -126,6 +125,38 @@ class TokenManipulator implements ManipulatorInterface return $this->create($user, self::TYPE_FEED_ENTRY, null, $entry->getId()); } + /** + * Create feedEntryTokens for many users in one shot + * + * @param User[] $users + * @param FeedEntry $entry + * @return Token[] + * @throws \Doctrine\DBAL\DBALException + */ + public function createFeedEntryTokens($users, FeedEntry $entry) + { + // $this->removeExpiredTokens(); + + $tokens = []; + foreach ($users as $user) { + $value = $this->random->generateString(32, self::LETTERS_AND_NUMBERS) . $user->getId(); + + $token = new Token(); + $token->setUser($user) + ->setType(self::TYPE_FEED_ENTRY) + ->setValue($value) + ->setExpiration(null) + ->setData($entry->getId()); + $tokens[] = $token; + + $this->om->persist($token); + } + $this->om->flush(); + $this->om->clear(); + + return $tokens; + } + /** * @param User $user * @param $data diff --git a/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php index ad45425f7c..8e40f9ebd8 100644 --- a/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php +++ b/lib/Alchemy/Phrasea/Model/Repositories/TokenRepository.php @@ -72,4 +72,9 @@ class TokenRepository extends EntityRepository return $query->getResult(); } + + public function getEntityManager() + { + return parent::getEntityManager(); + } } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php index 63d9ec108e..2424330031 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php @@ -668,17 +668,20 @@ class ElasticSearchEngine implements SearchEngineInterface } // fields aggregates $structure = $this->context_factory->getLimitedStructure($options); - foreach ($structure->getFacetFields() as $name => $field) { - // 2015-05-26 (mdarse) Removed databox filtering. - // It was already done by the ACL filter in the query scope, so no - // document that shouldn't be displayed can go this far. - $agg = [ - 'terms' => [ - 'field' => $field->getIndexField(true), - 'size' => $field->getFacetValuesLimit() - ] - ]; - $aggs[$name] = AggregationHelper::wrapPrivateFieldAggregation($field, $agg); + foreach($structure->getAllFields() as $name => $field) { + $size = $this->options->getAggregableFieldLimit($name); + if ($size !== databox_field::FACET_DISABLED) { + if ($size === databox_field::FACET_NO_LIMIT) { + $size = ESField::FACET_NO_LIMIT; + } + $agg = [ + 'terms' => [ + 'field' => $field->getIndexField(true), + 'size' => $size + ] + ]; + $aggs[$name] = AggregationHelper::wrapPrivateFieldAggregation($field, $agg); + } } return $aggs; diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchOptions.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchOptions.php index 0532f85c30..07e54449eb 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchOptions.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchOptions.php @@ -9,6 +9,10 @@ */ namespace Alchemy\Phrasea\SearchEngine\Elastic; +use databox_field; +use igorw; + + class ElasticsearchOptions { const POPULATE_ORDER_RID = "RECORD_ID"; @@ -35,7 +39,7 @@ class ElasticsearchOptions private $populateDirection; /** @var int[] */ - private $_customValues; + private $_customValues = []; private $activeTab; /** @@ -57,14 +61,10 @@ class ElasticsearchOptions 'populate_order' => self::POPULATE_ORDER_RID, 'populate_direction' => self::POPULATE_DIRECTION_DESC, 'activeTab' => null, + 'facets' => [] ]; - - foreach(self::getAggregableTechnicalFields() as $k => $f) { - $defaultOptions[$k.'_limit'] = 0; - } $options = array_replace($defaultOptions, $options); - $self = new self(); $self->setHost($options['host']); $self->setPort($options['port']); @@ -76,11 +76,10 @@ class ElasticsearchOptions $self->setPopulateOrder($options['populate_order']); $self->setPopulateDirection($options['populate_direction']); $self->setActiveTab($options['activeTab']); - foreach(self::getAggregableTechnicalFields() as $k => $f) { - $self->setAggregableFieldLimit($k, $options[$k.'_limit']); + foreach($options['facets'] as $fieldname=>$attributes) { + $self->setAggregableField($fieldname, $attributes); } - return $self; } @@ -99,10 +98,11 @@ class ElasticsearchOptions 'highlight' => $this->highlight, 'populate_order' => $this->populateOrder, 'populate_direction' => $this->populateDirection, - 'activeTab' => $this->activeTab + 'activeTab' => $this->activeTab, + 'facets' => [] ]; - foreach(self::getAggregableTechnicalFields() as $k => $f) { - $ret[$k.'_limit'] = $this->getAggregableFieldLimit($k); + foreach($this->getAggregableFields() as $fieldname=>$attributes) { + $ret['facets'][$fieldname] = $attributes; } return $ret; @@ -222,12 +222,51 @@ class ElasticsearchOptions public function setAggregableFieldLimit($key, $value) { - $this->_customValues[$key.'_limit'] = $value; + if(is_null($this->getAggregableField($key))) { + $this->_customValues['facets'][$key] = []; + } + $this->_customValues['facets'][$key]['limit'] = $value; + } + + public function setAggregableField($key, $attributes) + { + $this->getAggregableFields(); // ensure facets exists + $this->_customValues['facets'][$key] = $attributes; } public function getAggregableFieldLimit($key) { - return $this->_customValues[$key.'_limit']; + $facet = $this->getAggregableField($key); + return (is_array($facet) && array_key_exists('limit', $facet)) ? $facet['limit'] : databox_field::FACET_DISABLED; + } + + public function getAggregableField($key) + { + $facets = $this->getAggregableFields(); + return array_key_exists($key, $facets) ? $facets[$key] : null; + } + + /** + * @return array + */ + public function getAggregableFields() + { + if(!array_key_exists('facets', $this->_customValues) || !is_array($this->_customValues['facets'])) { + $this->_customValues['facets'] = []; + } + return $this->_customValues['facets']; + } + + // set to change the facets order during admin/form save + public function reorderAggregableFields($facetNames) + { + $newFacets = []; + foreach ($facetNames as $name) { + if(($facet = $this->getAggregableField($name)) !== null) { + $newFacets[$name] = $facet; + } + } + $this->_customValues['facets'] = $newFacets; } public function getActiveTab() @@ -241,56 +280,56 @@ class ElasticsearchOptions public function __get($key) { - if(!array_key_exists($key, $this->_customValues)) { - $this->_customValues[$key] = 0; - } - return $this->_customValues[$key]; + $keys = explode(':', $key); + + return igorw\get_in($this->_customValues, $keys); } public function __set($key, $value) { - $this->_customValues[$key] = $value; + $keys = explode(':', $key); + $this->_customValues = igorw\assoc_in($this->_customValues, $keys, $value); } public static function getAggregableTechnicalFields() { return [ - 'base_aggregate' => [ + '_base' => [ 'type' => 'string', 'label' => 'prod::facet:base_label', 'field' => "database", 'esfield' => 'databox_name', 'query' => 'database:%s', ], - 'collection_aggregate' => [ + '_collection' => [ 'type' => 'string', 'label' => 'prod::facet:collection_label', 'field' => "collection", 'esfield' => 'collection_name', 'query' => 'collection:%s', ], - 'doctype_aggregate' => [ + '_doctype' => [ 'type' => 'string', 'label' => 'prod::facet:doctype_label', 'field' => "type", 'esfield' => 'type', 'query' => 'type:%s', ], - 'camera_model_aggregate' => [ + '_camera_model' => [ 'type' => 'string', 'label' => 'Camera Model', 'field' => "meta.CameraModel", 'esfield' => 'metadata_tags.CameraModel', 'query' => 'meta.CameraModel:%s', ], - 'iso_aggregate' => [ + '_iso' => [ 'type' => 'number', 'label' => 'ISO', 'field' => "meta.ISO", 'esfield' => 'metadata_tags.ISO', 'query' => 'meta.ISO=%s', ], - 'aperture_aggregate' => [ + '_aperture' => [ 'type' => 'number', 'label' => 'Aperture', 'field' => "meta.Aperture", @@ -300,7 +339,7 @@ class ElasticsearchOptions return round($value, 1); }, ], - 'shutterspeed_aggregate' => [ + '_shutterspeed' => [ 'type' => 'number', 'label' => 'Shutter speed', 'field' => "meta.ShutterSpeed", @@ -313,7 +352,7 @@ class ElasticsearchOptions return $value . ' s.'; }, ], - 'flashfired_aggregate' => [ + '_flashfired' => [ 'type' => 'boolean', 'label' => 'FlashFired', 'field' => "meta.FlashFired", @@ -327,49 +366,49 @@ class ElasticsearchOptions return array_key_exists($value, $map) ? $map[$value] : $value; }, ], - 'framerate_aggregate' => [ + '_framerate' => [ 'type' => 'number', 'label' => 'FrameRate', 'field' => "meta.FrameRate", 'esfield' => 'metadata_tags.FrameRate', 'query' => 'meta.FrameRate=%s', ], - 'audiosamplerate_aggregate' => [ + '_audiosamplerate' => [ 'type' => 'number', 'label' => 'Audio Samplerate', 'field' => "meta.AudioSamplerate", 'esfield' => 'metadata_tags.AudioSamplerate', 'query' => 'meta.AudioSamplerate=%s', ], - 'videocodec_aggregate' => [ + '_videocodec' => [ 'type' => 'string', 'label' => 'Video codec', 'field' => "meta.VideoCodec", 'esfield' => 'metadata_tags.VideoCodec', 'query' => 'meta.VideoCodec:%s', ], - 'audiocodec_aggregate' => [ + '_audiocodec' => [ 'type' => 'string', 'label' => 'Audio codec', 'field' => "meta.AudioCodec", 'esfield' => 'metadata_tags.AudioCodec', 'query' => 'meta.AudioCodec:%s', ], - 'orientation_aggregate' => [ + '_orientation' => [ 'type' => 'string', 'label' => 'Orientation', 'field' => "meta.Orientation", 'esfield' => 'metadata_tags.Orientation', 'query' => 'meta.Orientation=%s', ], - 'colorspace_aggregate' => [ + '_colorspace' => [ 'type' => 'string', 'label' => 'Colorspace', 'field' => "meta.ColorSpace", 'esfield' => 'metadata_tags.ColorSpace', 'query' => 'meta.ColorSpace:%s', ], - 'mimetype_aggregate' => [ + '_mimetype' => [ 'type' => 'string', 'label' => 'MimeType', 'field' => "meta.MimeType", diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchSettingsFormType.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchSettingsFormType.php index e1aaa3e378..1f2fa3d1e2 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchSettingsFormType.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticsearchSettingsFormType.php @@ -9,6 +9,7 @@ */ namespace Alchemy\Phrasea\SearchEngine\Elastic; +use Alchemy\Phrasea\SearchEngine\Elastic\Structure\GlobalStructure; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; @@ -17,6 +18,18 @@ use Symfony\Component\Validator\Constraints\Range; class ElasticsearchSettingsFormType extends AbstractType { + /** @var GlobalStructure */ + private $globalStructure; + + /** @var ElasticsearchOptions */ + private $esSettings; + + public function __construct(GlobalStructure $g, ElasticsearchOptions $settings) + { + $this->globalStructure = $g; + $this->esSettings = $settings; + } + public function buildForm(FormBuilderInterface $builder, array $options) { $builder @@ -56,59 +69,89 @@ class ElasticsearchSettingsFormType extends AbstractType ->add('minScore', 'integer', [ 'label' => 'Thesaurus Min score', 'constraints' => new Range(['min' => 0]), - ]); + ]) + ->add('highlight', 'checkbox', [ + 'label' => 'Activate highlight', + 'required' => false + ]) + // ->add('save', 'submit', [ + // 'attr' => ['class' => 'btn btn-primary'] + // ]) + ->add('esSettingFromIndex', 'button', [ + 'label' => 'Get setting form index', + 'attr' => [ + 'onClick' => 'esSettingFromIndex()', + 'class' => 'btn' + ] + ]) + ->add('dumpField', 'textarea', [ + 'label' => false, + 'required' => false, + 'mapped' => false, + 'attr' => ['class' => 'dumpfield hide'] + ]) + ->add('activeTab', 'hidden'); - foreach(ElasticsearchOptions::getAggregableTechnicalFields() as $k => $f) { - if(array_key_exists('choices', $f)) { - // choices[] : choice_key => choice_value - $choices = $f['choices']; - } - else { - $choices = [ - "10 values" => 10, - "20 values" => 20, - "50 values" => 50, - "100 values" => 100, - "all values" => -1 - ]; - } - // array_unshift($choices, "not aggregated"); // always as first choice - $choices = array_merge(["not aggregated" => 0], $choices); - $builder - ->add($k.'_limit', ChoiceType::class, [ - // 'label' => $f['label'],// . ' ' . 'aggregate limit', - 'choices_as_values' => true, - 'choices' => $choices, - 'attr' => [ - 'class' => 'aggregate' - ] - ]); + // keep aggregates in configuration order with this intermediate array + $aggs = []; + + // helper fct to add aggregate to a tmp list + $addAgg = function($k, $label, $help, $disabled=false, $choices=null) use (&$aggs) { + if(!$choices) { + $choices = [ + "10 values" => 10, + "50 values" => 50, + "100 values" => 100, + "all values" => -1 + ]; } + $choices = array_merge(["not aggregated" => 0], $choices); // add this option always as first choice + $aggs[$k] = [ // default value will be replaced by hardcoded tech fields & all databoxes fields + 'label' => $label, + 'choices_as_values' => true, + 'choices' => $choices, + 'attr' => [ + 'class' => 'aggregate' + ], + 'disabled' => $disabled, + 'help_message' => $help // todo : not displayed ? + ]; + }; - $builder - ->add('highlight', 'checkbox', [ - 'label' => 'Activate highlight', - 'required' => false - ]) -// ->add('save', 'submit', [ -// 'attr' => ['class' => 'btn btn-primary'] -// ]) - ->add('esSettingFromIndex', 'button', [ - 'label' => 'Get setting form index', - 'attr' => [ - 'onClick' => 'esSettingFromIndex()', - 'class' => 'btn' - ] - ]) - ->add('dumpField', 'textarea', [ - 'label' => false, - 'required' => false, - 'mapped' => false, - 'attr' => ['class' => 'dumpfield hide'] - ]) - ->add('activeTab', 'hidden'); + // all fields fron conf + foreach($this->esSettings->getAggregableFields() as $k=>$f) { + // default value will be replaced by hardcoded tech fields & all databoxes fields + $addAgg($k, "/?\\ " . $k, "This field does not exists in current databoxes.", true); + } + + // add or replace hardcoded tech fields + foreach(ElasticsearchOptions::getAggregableTechnicalFields() as $k => $f) { + $choices = array_key_exists('choices', $f) ? $f['choices'] : null; // a tech-field can publish it's own choices + $help = null; + $label = '#' . $k; + if(!array_key_exists($k, $aggs)) { + $label = "/!\\ " . $label; + $help = "New field, please confirm setting."; + } + $addAgg($k, $label, $help, false, $choices); + } + + // add or replace all databoxes fields (nb: new db field - unknown in conf - will be a the end) + foreach($this->globalStructure->getAllFields() as $field) { + $k = $label = $field->getName(); + $help = null; + if(!array_key_exists($field->getName(), $aggs)) { + $label = "/!\\ " . $label; + $help = "New field, please confirm setting."; + } + $addAgg($k, $label, $help); // default choices + } + + // populate aggs to form + foreach($aggs as $k=>$agg) { + $builder->add('facets:' . $k . ':limit', ChoiceType::class, $agg); + } - ; } public function getName() diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/FacetsResponse.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/FacetsResponse.php index 2f2d966c51..efcc6d1c76 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/FacetsResponse.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/FacetsResponse.php @@ -15,7 +15,7 @@ class FacetsResponse private $escaper; private $facets = array(); - public function __construct(Escaper $escaper, array $response, GlobalStructure $structure) + public function __construct(ElasticsearchOptions $options, Escaper $escaper, array $response, GlobalStructure $structure) { $this->escaper = $escaper; @@ -25,7 +25,13 @@ class FacetsResponse $atf = ElasticsearchOptions::getAggregableTechnicalFields(); - foreach ($response['aggregations'] as $name => $aggregation) { + // sort facets respecting the order defined in options + foreach($options->getAggregableFields() as $name=>$foptions) { + if(!array_key_exists($name, $response['aggregations'])) { + continue; + } + $aggregation = $response['aggregations'][$name]; + $tf = null; $valueFormatter = function($v){ return $v; }; // default equality formatter @@ -78,6 +84,7 @@ class FacetsResponse ]; } } + } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php index ea4022dffb..0793c91c7b 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php @@ -35,11 +35,6 @@ final class GlobalStructure implements Structure */ private $private = array(); - /** - * @var Field[] - */ - private $facets = array(); - /** * @var Flag[] */ @@ -145,9 +140,11 @@ final class GlobalStructure implements Structure $this->private[$name] = $field; } + /* if ($field->isFacet() && $field->isSearchable()) { $this->facets[$name] = $field; } + */ if ($field->hasConceptInference()) { $this->thesaurus_fields[$name] = $field; @@ -183,14 +180,6 @@ final class GlobalStructure implements Structure return $this->private; } - /** - * @return Field[] - */ - public function getFacetFields() - { - return $this->facets; - } - /** * @return Field[] */ diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php index 671bf87c93..053ca6b0e0 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php @@ -47,14 +47,6 @@ final class LimitedStructure implements Structure return $this->limit($this->structure->getPrivateFields()); } - /** - * @return Field[] - */ - public function getFacetFields() - { - return $this->limit($this->structure->getFacetFields()); - } - public function getThesaurusEnabledFields() { return $this->limit($this->structure->getThesaurusEnabledFields()); diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php index 3c2be701e1..44d58e9d5f 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php @@ -33,11 +33,6 @@ interface Structure */ public function getPrivateFields(); - /** - * @return Field[] - */ - public function getFacetFields(); - /** * @return Field[] */ diff --git a/lib/classes/patch/410alpha20a.php b/lib/classes/patch/410alpha21a.php similarity index 96% rename from lib/classes/patch/410alpha20a.php rename to lib/classes/patch/410alpha21a.php index ff02a35c96..58091e62f2 100644 --- a/lib/classes/patch/410alpha20a.php +++ b/lib/classes/patch/410alpha21a.php @@ -11,10 +11,10 @@ use Alchemy\Phrasea\Application; -class patch_410alpha20a implements patchInterface +class patch_410alpha21a implements patchInterface { /** @var string */ - private $release = '4.1.0-alpha.20a'; + private $release = '4.1.0-alpha.21a'; /** @var array */ private $concern = [base::DATA_BOX]; diff --git a/lib/classes/patch/410alpha22a.php b/lib/classes/patch/410alpha22a.php new file mode 100644 index 0000000000..6a80bd058e --- /dev/null +++ b/lib/classes/patch/410alpha22a.php @@ -0,0 +1,75 @@ +release; + } + + /** + * {@inheritdoc} + */ + public function concern() + { + return $this->concern; + } + + /** + * {@inheritdoc} + */ + public function require_all_upgrades() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function getDoctrineMigrations() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function apply(base $appbox, Application $app) + { + foreach(['type', 'created', 'updated', 'expiration'] as $t) { + $sql = "ALTER TABLE `Tokens` ADD INDEX `".$t."` (`".$t."`);"; + try { + $stmt = $appbox->get_connection()->prepare($sql); + $stmt->execute(); + $stmt->closeCursor(); + } + catch (\Exception $e) { + // the inex already exists ? + } + } + + return true; + } +} diff --git a/templates/web/admin/fields/index.html.twig b/templates/web/admin/fields/index.html.twig index e8198a324f..531ef168eb 100644 --- a/templates/web/admin/fields/index.html.twig +++ b/templates/web/admin/fields/index.html.twig @@ -27,3 +27,17 @@ {# bootstrap admin field backbone application #} + + diff --git a/templates/web/admin/fields/templates.html.twig b/templates/web/admin/fields/templates.html.twig index 0dc661fc5c..73106dbafb 100644 --- a/templates/web/admin/fields/templates.html.twig +++ b/templates/web/admin/fields/templates.html.twig @@ -238,14 +238,12 @@ - + <%= field['aggregable'] == "0" ? '{% trans %}Not aggregated{% endtrans %}' : '' %> + <%= field['aggregable'] == "10" ? '10 values' : '' %> + <%= field['aggregable'] == "20" ? '20 values' : '' %> + <%= field['aggregable'] == "50" ? '50 values' : '' %> + <%= field['aggregable'] == "100" ? '100 values' : '' %> + <%= field['aggregable'] == "-1" ? 'All values' : '' %> @@ -261,11 +259,14 @@ - - - - - /> + + +
> + +
+ diff --git a/templates/web/admin/search-engine/general-aggregation.html.twig b/templates/web/admin/search-engine/general-aggregation.html.twig index d4b83ba31b..0c377d2ecd 100644 --- a/templates/web/admin/search-engine/general-aggregation.html.twig +++ b/templates/web/admin/search-engine/general-aggregation.html.twig @@ -1,12 +1,37 @@
+ +
+ {#{{ 'See' | trans }} : #} + + + + + + + +