Hi I am using a SAutoComplete (extends CAutoComplete) and need to do some work when a value is selected from the list.
i am using it like this.
this->widget('application.components.SAutoComplete', array('width'=>200,
'model'=>$cssAtapsClient, 'parseData'=>true, 'matchContains'=>true,
'attribute'=>'suburb_id', 'data'=>$postCode, 'ddindicator'=>true,
'max'=>50,
'multipleSeparator'=>false,
'options' => array(
'select' => new CJavaScriptExpression('function(e, ui) { alert("hi"); }')
),
)); ?>
i am wondering why there is no select option like available in jquery UI auto completed?
example of a select is as below.
$("#auto_cp").autocomplete({
minLength: 3,
//source
source: function(req, add) {
$.getJSON("friends.php?callback=?", req, function(data) {
var suggestions = [];
$.each(data, function(i, val) {
suggestions.push({
label: val.name,
zzz: val.zzz
});
});
add(suggestions);
});
},
//select
select: function(e, ui) {
alert(ui.item.zzz);
}
});
EDIT 2
http://www.yiiframework.com/doc/api/1.1/CAutoComplete
code is like this,
<?php
class SAutoComplete extends CAutoComplete
{
public $ddindicator;
/**
*
* #var boolean whether to parse the data assumes data uses an assoc array
* array(value => array(name, value, ...), ...). Only works with a model present
*/
public $parseData;
/**
* #var boolean whether to raise the change event
*/
public $raiseChangeEvent = false;
/**
* Initializes the widget.
* This method registers all needed client scripts and renders
* the autocomplete input.
*/
public function init()
{
if ( !$this->max )
$this->max = 50000;
if ( $this->ddindicator )
$this->alternateInit();
else
parent::init();
}
public function alternateInit()
{
list($name,$id)=$this->resolveNameID();
$this->htmlOptions['id'] = $id.'_input';
$this->minChars = 0;
echo CHtml::openTag('div', array('class'=>'ac-input-dd'));
echo CHtml::openTag('div', array('class'=>'ac-input-btn'));
echo CHtml::closeTag('div');
if($this->hasModel())
{
$htmlOpt = array();
if ( $this->parseData )
{
$menu = $this->data;
$key = $this->attribute;
//Change if attribute is apart of a array. eg attribute[0]
$pos1 = stripos($key, '[');
$pos2 = stripos($key, ']');
if($pos1!==false && $pos2!==false)
{
$key = str_replace (substr($key,$pos1,$pos2 - $pos1 + 1),'',$key);
$htmlOpt['value'] = isset($this->model->$key) ? $this->model->$key : '';
}
$this->value = isset($menu[$this->model->$key][0]) ? $menu[$this->model->$key][0] : '';
$this->data = is_array($menu) ? array_values($menu) : array('Error in data.');
}
echo CHtml::activeHiddenField($this->model, $this->attribute, array_merge(array('id'=>$id, 'name'=>$name), $htmlOpt));
echo CHtml::textField('', $this->value, $this->htmlOptions);
}
else
{
echo CHtml::hiddenField($name, $this->value, array('id'=>$id));
echo CHtml::textField($name.'_input',$this->value,$this->htmlOptions);
}
echo CHtml::closeTag('div');
$this->methodChain = $this->methodChain.'.result(function(evt, data, formatted) { $("#'.
$id.'").val(data ? data[1] : "")'.($this->raiseChangeEvent?'.change()':'').'; })'.
'.parent().find(".ac-input-btn").mousedown(function(){'.
'jQuery(this).parent().find(".ac_input").toggleResults();})'.
'.mouseup(function(){jQuery(this).parent().find(".ac_input").focus();});';
$this->registerClientScript();
}
public static function registerScript()
{
$cs = Yii::app()->getClientScript();
$cs->registerCoreScript('jquery');
$cs->registerCoreScript('bgiframe');
TK::registerScriptFile('autocomplete');
$cs->registerCssFile($cs->getCoreScriptUrl().'/autocomplete/jquery.autocomplete.css');
}
/**
* Registers the needed CSS and JavaScript.
* #since 1.0.1
*/
public function registerClientScript()
{
// can cut this down once YII releases a fix for defect #38
if ( Yii::app()->request->isAjaxRequest || $this->ddindicator )
{
$id=$this->htmlOptions['id'];
$acOptions=$this->getClientOptions();
$options=$acOptions===array()?'{}' : CJavaScript::encode($acOptions);
$cs=Yii::app()->getClientScript();
if($this->data!==null)
$data=CJavaScript::encode($this->data);
else
{
$url=CHtml::normalizeUrl($this->url);
$data='"'.$url.'"';
}
if ( Yii::app()->request->isAjaxRequest )
{
echo '<script type="text/javascript">jQuery(document).ready('.
'function() {jQuery("#'.$id.'").autocomplete('.$data.','.$options.')'.
$this->methodChain.';});</script>';
}
else
{
SAutoComplete::registerScript();
$cs->registerScript('Yii.CAutoComplete#'.$id,"jQuery(\"#{$id}\").autocomplete($data,{$options}){$this->methodChain};");
}
}
else
parent::registerClientScript();
}
}
You can pass any option that the JUI autocomplete widget supports by including an options array:
$this->widget('zii.widgets.jui.CJuiAutoComplete', array(
// your other settings here
'options' => array(
'select' => new CJavaScriptExpression('function(e, ui) { alert("hi"); }')
),
));
If the options you want to pass include JavaScript code then you also have to wrap that inside a CJavaScriptExpression as above.
Try to add this,
'methodChain'=>".result(function(event,item){ urFunction(); })",
Have a nice day!
So at last it will look like,
$this->widget('application.components.SAutoComplete', array('width'=>200,
'model'=>$cssAtapsClient, 'parseData'=>true, 'matchContains'=>true,
'attribute'=>'suburb_id', 'data'=>$postCode, 'ddindicator'=>true,
'max'=>50,
'methodChain'=>".result(function(event,item){ urFucntion(); })",
));
I'm using CAutoComplete as the followin code:
In post/_form.php
<?php $this->widget('CAutoComplete', array(
'model' => $model,
'attribute' => 'tags',
'url' => array('suggestTags'),
'multiple' => true,
'htmlOptions' => array(
'size' => 50,
'class' => 'span11'
),
)); ?>
In PostController.php
Add one more action call: suggestTags
/**
* Suggests tags based on the current user input.
* This is called via AJAX when the user is entering the tags input.
*/
public function actionSuggestTags()
{
if(isset($_GET['q']) && ($keyword=trim($_GET['q']))!=='')
{
$tags=Tag::model()->suggestTags($keyword);
if($tags!==array())
echo implode("\n",$tags);
}
}
In Post.php model
add private $_oldTags; to the top of class (under class name).
add these functions:
/**
* Normalizes the user-entered tags.
*/
public function normalizeTags($attribute, $params)
{
$this->tags = Post::array2string(array_unique(Post::string2array($this->tags)));
}
public static function string2array($tags)
{
return preg_split('/\s*,\s*/', trim($tags), -1, PREG_SPLIT_NO_EMPTY);
}
public static function array2string($tags)
{
return implode(', ', $tags);
}
See more Yii tutorials here
Related
A couple years ago, I setup a paginated list of News Article Pages that were sorted by year based on a dropdown field selection: SilverStripe - Create pagination based on dropdown selection
I'm now trying to do the same thing but this time, the News Articles are data objects. Otherwise, everything else is the same.
The problem is I cannot get it to work this time and I'm not sure why. I'm following the same steps but when I select a year from the dropdown, I am taken to a 404 page on my site, and, in the Network tab in Chrome, there is a 404 status for the url:
I can see that the year value is not getting passed to the page's code for processing, but I'm not sure why as it did work -- and still works -- on the other SilverStripe site I was working on from the old post I linked to.
Here is the code that I have right now:
;(function($) {
$(function(){
// hide form actions, as we want to trigger form submittal
var newsYearFilterForm = $("#Form_YearFilterForm");
// automatically when dropdown changes
newsYearFilterForm.find(".Actions").hide();
// bind a change event on the dropdown to automatically submit
newsYearFilterForm.on("change", 'select[name="Year"]', function(e){
newsYearFilterForm.submit();
});
// handle pagination clicks
$("body").on("click", "a.pagination", function (e) {
e.preventDefault();
// $("#ArticleList").addClass("loading");
$.get(
$(this).attr("href"),
function(data, status, xhr){
$("#ArticleList").replaceWith($(data));
}
);
return false;
});
});
})(jQuery);
NewsLandingPage.php
<?php
class NewsLandingPage extends Page
{
private static $description = 'News Landing page.';
private static $db = array();
private static $has_one = array();
}
class NewsLandingPage_Controller extends Page_Controller
{
private static $allowed_actions = array(
'renderNewsItem',
'YearFilterForm',
'year',
);
public function init()
{
parent::init();
}
private static $url_handlers = array(
'$NewsItem!' => 'renderNewsItem',
);
public function getAllNews()
{
$newsList = NewsItem::get()->sort('NewsDate', 'DESC');
return new PaginatedList($newsList, $this->getRequest());
}
public function renderNewsItem(SS_HTTPRequest $request)
{
$newsItemName = $request->param('NewsItem');
if ($newsItemName != "") {
$newsItem = NewsItem::get()->filterAny(array(
'NewsItemSlug' => $newsItemName
))->first();
if ($newsItem) {
$arrayData = array(
'NewsItem' => $newsItem,
);
return $this->renderWith(array('NewsArticle', 'Page'), new ArrayData($arrayData));
} else {
return $this->httpError(404);
}
}
}
public function handleYearRequest(SS_HTTPRequest $request)
{
$year = $request->param('ID');
$data = array(
'Year' => $year,
'PaginatedReleases' => $this->PaginatedReleases($year)
);
if ($request->isAjax()) {
// in case of an ajax request, render only the partial template
return $this->renderWith('ArticleList', $data);
} else {
// returning an array will cause the page to render normally
return $data;
}
}
//creates a form to filter through news releases by year
public function YearFilterForm()
{
// get an array of all distinct years
$list = SQLSelect::create()
->addFrom('NewsItem')
->selectField('YEAR("NewsDate")', 'Year')
->setOrderBy('Year', 'DESC')
->execute()->column('Year');
// create an associative array with years as keys & values
$values = array_combine($list, $list);
// our fields just contain the dropdown, which uses the year values
$fields = FieldList::create(array(
DropdownField::create(
'Year',
'',
$values,
$this->getRequest()->param('ID')
)->setHasEmptyDefault(true)
->setEmptyString('Show all')
->setAttribute('data-urlpattern', $this->Link('year') . '/YYYY')
));
$actions = new FieldList(
FormAction::create('doFilter', 'Submit')
);
return Form::create($this, 'YearFilterForm', $fields, $actions);
}
public function year()
{
return $this->handleYearRequest($this->request);
}
public function index()
{
return $this->handleYearRequest($this->request);
}
//redirects to the proper url depending on which year is selected for sorting news
public function doFilter($data, $form)
{
if (empty($data['Year'])) {
return $this->redirect($this->Link());
} else {
return $this->redirect($this->Link('year/' . $data['Year']));
}
}
//created a paginated list of news released by year
public function PaginatedReleases($year = null)
{
$list = NewsItem::get()->sort('NewsDate', 'DESC');
if ($year) {
$list = $list->where(array('YEAR("NewsDate") = ?' => $year));
}
return PaginatedList::create($list, $this->getRequest())->setLimitItems(10);
}
}
NewsItem.php
class NewsItem extends DataObject{
private static $db = array(
'NewsName' => 'Varchar(255)',
'NewsItemSlug' => 'Varchar(250)',
'NewsDate' => 'SS_Datetime',
'NewsLocation' => 'Varchar(255)',
'NewsArticle' => 'HTMLText',
'NewsArticleSummary' => 'HTMLText',
'SortOrder' => 'Int',
);
private static $summary_fields = array(
'NewsName',
'NewsDate',
'NewsItemSlug',
);
private static $has_one = array(
'NewsImage' => 'Image',
'NewsUrlForHomePage' => 'SiteTree',
'Page' => 'Page'
);
public function onBeforeWrite() {
parent::onBeforeWrite();
if ($this->NewsItemSlug == ""){
$linkName = $this::getLinkName();
if ($linkName == ""){
$linkName = str_replace(array(" ",":","%","$","#","#","!","^","&","*","(",")","'",";","<",">","/","?","[","]","{","}","\\","|","`","~","=","+","’",",","."),"-",strtolower(str_replace("&","and",str_replace(".","",$this->NewsName))));
}
$this->NewsItemSlug = $linkName;
} else {
$this->NewsItemSlug = str_replace(array(" ",":","%","$","#","#","!","^","&","*","(",")","'",";","<",">","/","?","[","]","{","}","\\","|","`","~","=","+","’",",","."),"-",strtolower(str_replace("&","and",str_replace(".","",$this->NewsItemSlug))));
}
}
public function getLinkName() {
if ($this->NewsItemSlug != "" && !is_null($this->NewsItemSlug)){
return $this->NewsItemSlug;
}
return str_replace(" ","-",strtolower(str_replace("&","and",$this->NewsName)));
}
public function LinkingMode() {
return Controller::curr()->getRequest()->param('ID') == $this::getLinkName() ? 'current' : 'link';
}
public function Link() {
$newsLandingPage = NewsLandingPage::get()->first();
$linkName = $this::getLinkName();
if ($linkName){
return $newsLandingPage->Link($linkName);
} else {
return false;
}
}
public function canView($member = null){
return true;
}
public function canEdit($member = null) {
return true;
}
public function canCreate($member = null) {
return true;
}
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","PageID");
return $fields;
}
}
class NewsItemAdmin extends ModelAdmin {
private static $managed_models = array(
'NewsItem',
);
private static $url_segment = 'NewsItems';
private static $menu_title = 'News';
}
NewsLandingPage.ss
<% include BreadCrumbs %>
<div class="container container-intro-copy" style="background-color:#fff;opacity:1.0;">
<h1 class="intro-copy h1">$H1</h1>
<div class="intro-copy">$Content</div>
$YearFilterForm
<% include NewsList %>
</div>
NewsList.ss
<div id="ArticleList" class="container careers-list">
<div class="row">
<% loop $PaginatedReleases %>
<div class="career-item col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1 col-sm-10 col-sm-offset-1 col-xs-12 item bio-detail aos-init aos-animate" data-aos="fade-up" data-aos-delay="200" style="margin-top:30px;">
<div class="box-for-resources top-box-style">
<div class="box-resources-left"><img src="$NewsImage.URL" class="image-resources-cmmm"></div>
<div class="box-resources-right">
<h3 class="name left" style="color:#002f65 !important; float: left; clear: both;">$NewsName</h3>
<p class="box-resource-copy text-date"><em>$NewsDate.FormatI18N('%B %d, %Y') ($NewsLocation)</em></p><br />
<div class="careers-copy">$NewsArticleSummary</div><br />
Read Article
</div>
</div>
</div>
<% end_loop %>
</div>
</div>
The news page successfully loads all the articles by default and the links to each article work properly. The dropdown form has the correct list of years based on the range the articles have.
I think you have a conflict with your url_handlers and the actions you're calling on the controller.
'$NewsItem!' => 'renderNewsItem',
The above line matches all actions to renderNewsItem. Eg. yoursite.test/controller/YearFilterForm will also be matched by this…
You should add some static part to your handler that displays a news item. So your url_handlers would be:
'show/$NewsItem!' => 'renderNewsItem',
Then you'd have to adjust your NewsItem->Link method accordingly.
I also suggest you use the URLSegmentFilter to create your slugs… eg.
public function onBeforeWrite() {
parent::onBeforeWrite();
// only rebuild the slug when news-name has changed
if ($this->isChanged('NewsName', DataObject::CHANGE_VALUE)) {
$filter = URLSegmentFilter::create();
$t = $filter->filter($this->NewsName);
// Fallback to generic name if path is empty (= no valid, convertable characters)
if (!$t || $t == '-' || $t == '-1') {
$t = "news-{$this->ID}";
}
$this->NewsItemSlug = $t;
}
}
I want to integrate elasticsearch in my laravel project.
I have installed using following line :
Run command on terminal :
composer require shift31/laravel-elasticsearch:~1.0
Then i have created elasticsearch.php in app/config/ and added following code.
<?php
use Monolog\Logger;
return array(
'hosts' => array(
'your.elasticsearch.server:9200' // what should be my host ?
),
'logPath' => 'path/to/your/elasticsearch/log',
'logLevel' => Logger::INFO
);
My first question : What should i write in place of host name
Right now my project is running on local server with localhost:8000.
I have added Shift31\LaravelElasticsearch\ElasticsearchServiceProvider in app/config/app.php for enable the 'Es' facade.
Above all things done. Now in which file i should add the code of elasticsearch to add, update, delete and search the records.
I have product table I need to add product records in elasticsearch, when update product, records should be update.
I have no idea of the further process. Please guide me I have searched on google but no any example help me.
Create the following helper classes in their respective paths:
App\Traits\ElasticSearchEventTrait.php
<?php
Namespace App\Traits;
trait ElasticSearchEventTrait {
public $esRemoveDefault = array('created_at','updated_at','deleted_at');
public static function boot()
{
parent::boot();
static::bootElasticSearchEvent();
}
public static function bootElasticSearchEvent()
{
static::created(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esCreate();
}
});
static::updated(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
static::deleted(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
}
private function esCreate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if(isset($this->esMain) && $this->esMain === true && $this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#indexTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
else
{
$this->esUpdate();
}
}
private function esUpdate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if($this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#updateTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
}
/*
* Get Id of Model
*/
public function esGetId()
{
if(isset($this->esId))
{
return $this->esId;
}
else
{
return $this->id;
}
}
public function esGetInfoContext()
{
if(isset($this->esInfoContext))
{
return $this->esInfoContext;
}
else
{
throw new \RuntimeException("esInfoContext attribute or esGetInfoContext() is not set in class '".get_class($this)."'");
}
}
/*
* Name of main context of model
*/
public function esGetContext()
{
if(isset($this->esContext))
{
return $this->esContext;
}
else
{
throw new \RuntimeException("esContext attribute or esGetContext() method must be set in class '".get_class($this)."'");
}
}
/*
* All attributes that needs to be removed from model
*/
public function esGetRemove()
{
if(isset($this->esRemove))
{
return array_unique(array_merge($this->esRemoveDefault,$this->esRemove));
}
else
{
return $this->esRemoveDefault;
}
}
/*
* Extends Illuminate Collection to provide additional array functions
*/
public function newCollection(array $models = Array())
{
return new Core\Collection($models);
}
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
public function asEsDateTime($value)
{
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value))
{
return \Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value))
{
return \Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
}
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
elseif ( ! $value instanceof DateTime)
{
$format = $this->getEsDateFormat();
return \Carbon::createFromFormat($format, $value);
}
return \Carbon::instance($value);
}
/**
* Get the format for database stored dates.
*
* #return string
*/
private function getEsDateFormat()
{
return $this->getConnection()->getQueryGrammar()->getDateFormat();
}
/*
* Converts model to a suitable format for ElasticSearch
*/
public function getEsSaveFormat()
{
$obj = clone $this;
//Go through ES Accessors
\ElasticSearchHelper::esAccessor($obj);
$dates = $this->getDates();
//Convert to array, then change Date to appropriate Elasticsearch format.
//Why? Because eloquent's date accessors is playing me.
$dataArray = $obj->attributesToArray();
//Remove all Excludes
foreach($this->esGetRemove() as $ex)
{
if(array_key_exists($ex,$dataArray))
{
unset($dataArray[$ex]);
}
}
if(!empty($dates))
{
foreach($dates as $d)
{
if(isset($dataArray[$d]) && $dataArray[$d] !== "" )
{
//Trigger Eloquent Getter which will provide a Carbon instance
$dataArray[$d] = $this->{$d}->toIso8601String();
}
}
}
return $dataArray;
}
}
App\Services\ElasticServiceHelper.php
<?php
/**
* Description of ElasticSearchHelper: Helps with Indexing/Updating with Elastic Search Server (https://www.elastic.co)
*
* #author kpudaruth
*/
Namespace App\Services;
class ElasticSearchHelper {
/*
* Laravel Queue - Index Task
* #param array $job
* #param array $data
*/
public function indexTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->indexEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Laravel Queue - Update Task
* #param array $job
* #param array $data
*/
public function updateTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->updateEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Index Elastic Search Document
* #param array $data
*/
public function indexEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::find($data['id']);
if($form)
{
$params['id'] = $form->id;
if($form->timestamps)
{
$params['timestamp'] = $form->updated_at->toIso8601String();
}
$params['body'][$data['context']] = $this->saveFormat($form);
\Es::index($params);
}
}
/*
* Update Elastic Search
* #param array $data
*/
public function updateEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::withTrashed()->find($data['id']);
if(count($form))
{
/*
* Main form is being updated
*/
if($data['info-context'] === $data['context'])
{
$params['id'] = $data['id'];
$params['body']['doc'][$data['info-context']] = $this->saveFormat($form);
}
else
{
//Form is child, we get parent
$parent = $form->esGetParent();
if(count($parent))
{
//Id is always that of parent
$params['id'] = $parent->id;
//fetch all children, given that we cannot save per children basis
$children = $parent->{$data['info-context']}()->get();
if(count($children))
{
//Get data in a format that can be saved by Elastic Search
$params['body']['doc'][$data['info-context']] = $this->saveFormat($children);
}
else
{
//Empty it is
$params['body']['doc'][$data['info-context']] = array();
}
}
else
{
\Log::error("Parent not found for {$data['context']} - {$data['class']}, Id: {$data['id']}");
return false;
}
}
//Check if Parent Exists
try
{
$result = \Es::get([
'id' => $params['id'],
'index' => $params['index'],
'type' => $data['context']
]);
} catch (\Exception $ex) {
if($ex instanceof \Elasticsearch\Common\Exceptions\Missing404Exception || $ex instanceof \Guzzle\Http\Exception\ClientErrorResponseException)
{
//if not, we set it
if (isset($parent) && $parent)
{
$this->indexEs([
'context' => $data['context'],
'class' => get_class($parent),
'id' => $parent->id,
]);
}
else
{
\Log::error('Unexpected error in updating elasticsearch records, parent not set with message: '.$ex->getMessage());
return false;
}
}
else
{
\Log::error('Unexpected error in updating elasticsearch records: '.$ex->getMessage());
return false;
}
}
\Es::update($params);
}
}
/*
* Iterate through all Es accessors of the model.
* #param \Illuminate\Database\Eloquent\Model $object
*/
public function esAccessor(&$object)
{
if(is_object($object))
{
$attributes = $object->getAttributes();
foreach($attributes as $name => $value)
{
$esMutator = 'get' . studly_case($name) . 'EsAttribute';
if (method_exists($object, $esMutator)) {
$object->{$name} = $object->$esMutator($object->{$name});
}
}
}
else
{
throw New \RuntimeException("Expected type object");
}
}
/*
* Iterates over a collection applying the getEsSaveFormat function
* #param mixed $object
*
* #return array
*/
public function saveFormat($object)
{
if($object instanceof \Illuminate\Database\Eloquent\Model)
{
return $object->getEsSaveFormat();
}
else
{
return array_map(function($value)
{
return $value->getEsSaveFormat();
}, $object->all());
}
}
}
A couple of gotchas from the above helper classes:
The default ElasticSearch index is set to the name of the App's Environment
The ..task() functions are meant for the old laravel 4.2 queue format. I've yet to port those to laravel 5.x. Same goes for the Queue::push commands.
Example
ElasticSearch Mapping:
[
'automobile' => [
"dynamic" => "strict",
'properties' => [
'automobile' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'manufacturer_name' => [
'type' => 'string',
],
'manufactured_on' => [
'type' => 'date'
]
]
],
'car' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'name' => [
'type' => 'string',
],
'model_id' => [
'type' => 'string'
]
]
],
"car-model" => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'description' => [
'type' => 'string',
],
'name' => [
'type' => 'string'
]
]
]
]
]
]
Top level document is called 'automobile'. Underneath it, you have 'automobile', 'car' & 'car-model'. Consider 'car' & 'car-model' as relations to the automobile. They are known as sub documents on elasticsearch. (See: https://www.elastic.co/guide/en/elasticsearch/guide/current/document.html)
Model: App\Models\Car.php
namespace App\Models;
class Car extends \Eloquent {
use \Illuminate\Database\Eloquent\SoftDeletingTrait;
use \App\Traits\ElasticSearchEventTrait;
protected $table = 'car';
protected $fillable = [
'name',
'serie',
'model_id',
'automobile_id'
];
protected $dates = [
'deleted_at'
];
/* Elastic Search */
//Indexing Enabled
public $esEnabled = true;
//Context for Indexing - Top Level name in the mapping
public $esContext = "automobile";
//Info Context - Secondary level name in the mapping.
public $esInfoContext = "car";
//The following fields will not be saved in elasticsearch.
public $esRemove = ['automobile_id'];
//Fetches parent relation of car, so that we can retrieve its id for saving in the appropriate elasticsearch record
public function esGetParent()
{
return $this->automobile;
}
/*
* Event Observers
*/
public static function boot() {
parent:: boot();
//Attach events to model on start
static::bootElasticSearchEvent();
}
/*
* ElasticSearch Accessor
*
* Sometimes you might wish to format the data before storing it in elasticsearch,
* The accessor name is in the format of: get + attribute's name camel case + EsAttribute
* The $val parameter will always be the value of the attribute that is being accessed.
*
* #param mixed $val
*/
/*
* Elasticsearch Accessor: Model Id
*
* Get the model name and save it
*
* #param int $model_id
* #return string
*/
public function getModelIdEsAttribute($model_id) {
//Fetch model from table
$model = \App\Models\CarModel::find($model_id);
if($model) {
//Return name of model if found
return $model->name;
} else {
return '';
}
}
/*
* Automobile Relationship: Belongs To
*/
public function automobile()
{
return $this->belongsTo('\App\Models\Automobile','automobile_id');
}
}
Example of Search Query:
/**
* Get search results
*
* #param string $search (Search string)
*
*/
public function getAll($search)
{
$params = array();
$params['index'] = App::environment();
//Declare your mapping names in the array which you wish to search on.
$params['type'] = array('automobile');
/*
* Build Query String
*/
//Exact match is favored instead of fuzzy ones
$params['body']['query']['bool']['should'][0]['match']['name']['query'] = $search;
$params['body']['query']['bool']['should'][0]['match']['name']['operator'] = "and";
$params['body']['query']['bool']['should'][0]['match']['name']['boost'] = 2;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['like_text'] = $search;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['fuzziness'] = 0.5;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['prefix_length'] = 2;
$params['body']['query']['bool']['minimum_should_match'] = 1;
//Highlight matches
$params['body']['highlight']['fields']['*'] = new \stdClass();
$params['body']['highlight']['pre_tags'] = array('<b>');
$params['body']['highlight']['post_tags'] = array('</b>');
//Exclude laravel timestamps
$params['body']['_source']['exclude'] = array( "*.created_at","*.updated_at","*.deleted_at");
/*
* Poll search server until we have some results
*/
$from_offset = 0;
$result = array();
//Loop through all the search results
do
{
try
{
$params['body']['from'] = $from_offset;
$params['body']['size'] = 5;
$queryResponse = \Es::search($params);
//Custom function to process the result
//Since we will receive a bunch of arrays, we need to reformat the data and display it properly.
$result = $this->processSearchResult($queryResponse);
$from_offset+= 5;
}
catch (\Exception $e)
{
\Log::error($e->getMessage());
return Response::make("An error occured with the search server.",500);
}
}
while (count($result) === 0 && $queryResponse['hits']['total'] > 0);
echo json_encode($result);
}
/*
* Format search results as necessary
* #param array $queryResponse
*/
private function processSearchResult(array $queryResponse)
{
$result = array();
//Check if we have results in the array
if($queryResponse['hits']['total'] > 0 && $queryResponse['timed_out'] === false)
{
//Loop through each result
foreach($queryResponse['hits']['hits'] as $line)
{
//Elasticsearch will highlight the relevant sections in your query in an array. The below creates a readable format with · as delimiter.
$highlight = "";
if(isset($line['highlight']))
{
foreach($line['highlight'] as $k=>$v)
{
foreach($v as $val)
{
$highlight[] = str_replace("_"," ",implode(" - ",explode(".",$k)))." : ".$val;
}
}
$highlight = implode(" · ",$highlight);
}
//Check the mapping type
switch($line['_type'])
{
case "automobile":
$result[] = array('icon'=>'fa-automobile',
'title'=> 'Automobile',
'id' => $line['_id'],
//name to be displayed on my search result page
'value'=>$line['_source'][$line['_type']]['name']." (Code: ".$line['_id'].")",
//Using a helper to generate the url. Build your own class.
'url'=>\App\Helpers\URLGenerator::generate($line['_type'],$line['_id']),
//And the highlights as formatted above.
'highlight'=>$highlight);
break;
}
}
}
return $result;
}
I'm having an issue with Knp, an AJAX request, and a filter. I think I'm doing something very wrong here, but I am not sure how exactly KnpPaginator works internally, and I don't have the time to figure it out on this project.
Anyway, basically, my page has an embedded controller which renders a table on the page. When paginator is called from twig, it returns the route to the container page, which results in paginator failing to work with my GET requests to that uri.
I'm not sure if any of you have come across this - I'm happy to listen if there is a better solution to the problem (I'm quite sure there is). Here is my code:
CONTROLLER
/**
* Just a shell page
*
* #Route("/postmanagement/index")
* #Template()
*
* #return array
*/
public function indexAction()
{
$form = $this->createForm(new FilterPostsType(), null, array(
'action' => $this->generateUrl('myblog_admin_postmanagement_filterposts'),
'method' => 'POST'
)
);
return array(
'form' => $form->createView()
);
}
/**
* Returns active posts and comments
*
* #param Request $request
*
* #return array
*/
public function defaultAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$posts = $em->getRepository('ModelBundle:Post')->findBy(array(
'active' => true
)
);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate($posts, $request->query->get('page', 1), 10);
return $this->render("AdminBundle:PostManagement:_ajax-panel.html.twig", array(
'isPost' => true,
'posts' => $posts,
'pagination' => $pagination
)
);
}
/**
* #param Request $request
*
* #Route("/postmanagement/filter")
*
* #return array
*/
public function filterPostsAction(Request $request)
{
$form = $this->createForm(new FilterPostType(), null, array(
'action' => $this->generateUrl('myblog_admin_postmanagement_filterposts'),
'method' => 'POST'
)
);
// if ($request->isMethod('POST')) {
$posts = null;
$form->handleRequest($request);
$data = $form->getData();
$posts = $this->get('myblog.admin_manager')->filterPosts($data);
switch ($data['type']) {
case 'post':
$isPost = true;
$isComment = false;
break;
case 'comment':
$isPost = false;
$isComment = true;
break;
}
// }
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate($posts, $request->query->get('page', 1), $data['maxresults']);
if (is_null($posts)) {
return new NotFoundHttpException();
} else {
return $this->render('AdminBundle:PostManagement:_ajax-panel.html.twig', array(
'posts' => $posts,
'isPost' => $isPost,
'isComment' => $isComment,
'pagination' => $pagination
)
);
}
}
I'm not posting the view here, since it is a simple render(controller(MyBundle:Controller:myAction)). As you can see, there is a form I'm submitting on the page, to filter the posts. That also poses a problem, since it seems paginator doesn't keep the query after I've run it through the filter.
Thanks for any help! I would love if someone has done this before and has come up with a better solution than my rather convoluted one (which also involves too many queries for my liking).
I figured it out.
If anyone else would like to paginate with InfiScr trigger + KNPPaginatorBundle + filter (PHP), use this JS:
/**
* Load more pagination handler
*/
var AjaxPagination = function (options) {
AjaxProt.call(this, options);
this.filter = options.filter;
this.toJoinEl = options.toJoinEl;
this.containerEl = options.containerEl;
this.navContainer = options.navContainer;
this.nextSelector = options.nextSelector;
this.uri = options.uri;
};
AjaxPagination.prototype = Object.create(AjaxProt.prototype);
AjaxPagination.prototype.init = function () {
var thisObj = this,
uri = thisObj.uri;
$(thisObj.navContainer).hide();
$(document).on(thisObj.event, thisObj.targetEl, function (e) {
e.preventDefault();
thisObj.ajaxRequest(uri);
});
};
AjaxPagination.prototype.ajaxRequest = function (uri) {
var thisObj = this,
page = $(this.nextSelector).attr('href').match(/\d+$/);
$('#filter_bets_page').val(page);
var data = $(this.filter).serialize(),
method = this.method;
console.log(data);
$.ajax({
url: uri,
data: data,
type: method,
success: function (data) {
thisObj.infiScrCallback(data);
}
});
};
AjaxPagination.prototype.infiScrCallback = function(data) {
var thisObj = this;
$(thisObj.navContainer).remove();
if (thisObj.toJoinEl) {
var filteredContent = $("<div>").append( $.parseHTML( data ) ).find( '.findable');
var newPagination = $("<div>").append( $.parseHTML( data ) ).find( 'div.pagination-hidden' );
$(thisObj.toJoinEl).append(filteredContent);
$(thisObj.containerEl).append(newPagination);
} else {
$(thisObj.containerEl).append(data).fadeIn();
}
if (!$(thisObj.nextSelector).length) {
$(thisObj.targetEl).fadeOut();
}
};
I’m trying to get my Ajax-Function to call a certain controller inside my Typo3 extension.
After several attempts, I decided to go with the same Ajax-Dispatcher that powermail uses. It does show an output, but it completely ignores the supposed controller and action.
There are two Controllers in my Extension and so far three actions:
Category=>list
Family=>list,compare
PHP:
<?php
namespace Vendor\Myextension\Utility;
use \TYPO3\CMS\Core\Utility\GeneralUtility;
class AjaxDispatcher {
/**
* configuration
*
* #var \array
*/
protected $configuration;
/**
* bootstrap
*
* #var \array
*/
protected $bootstrap;
/**
* Generates the output
*
* #return \string rendered action
*/
public function run() {
return $this->bootstrap->run('', $this->configuration);
}
/**
* Initialize Extbase
*
* #param \array $TYPO3_CONF_VARS The global array. Will be set internally
*/
public function __construct($TYPO3_CONF_VARS) {
$this->configuration = array(
'pluginName' => 'my_plugin',
'vendorName' => 'Vendor',
'extensionName' => 'myextension',
'controller' => 'Family',
'action' => 'compare',
'mvc' => array(
'requestHandlers' => array(
'TYPO3\CMS\Extbase\Mvc\Web\FrontendRequestHandler' => 'TYPO3\CMS\Extbase\Mvc\Web\FrontendRequestHandler'
)
),
'settings' => array()
);
$_POST['request']['action'] = 'compare';
$_POST['request']['controller'] = 'Family';
$this->bootstrap = new \TYPO3\CMS\Extbase\Core\Bootstrap();
$userObj = \TYPO3\CMS\Frontend\Utility\EidUtility::initFeUser();
$pid = (GeneralUtility::_GET('id') ? GeneralUtility::_GET('id') : 1);
$GLOBALS['TSFE'] = GeneralUtility::makeInstance(
'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController',
$TYPO3_CONF_VARS,
$pid,
0,
TRUE
);
$GLOBALS['TSFE']->connectToDB();
$GLOBALS['TSFE']->fe_user = $userObj;
$GLOBALS['TSFE']->id = $pid;
$GLOBALS['TSFE']->determineId();
$GLOBALS['TSFE']->getCompressedTCarray();
$GLOBALS['TSFE']->initTemplate();
$GLOBALS['TSFE']->getConfigArray();
$GLOBALS['TSFE']->includeTCA();
}
}
$eid = GeneralUtility::makeInstance('Vendor\Myextension\Utility\AjaxDispatcher', $GLOBALS['TYPO3_CONF_VARS']);
echo $eid->run();
Ajax:
var read = 'string';
var requestData = {'value': read};
var currentUrl = window.location;
$.ajax({
url: currentUrl,
type: 'POST',
data: {
eID: "ajaxDispatcherMyextension",
request: {
controller: 'Family',
action: 'compare',
arguments: {
'test': requestData
}
}
},
dataType: 'html',
success: function(success) {
console.log('success ' + success);
$('#test').html(success);
}
});
Instead of showing family->compare the Ajax puts out category->compare. I don’t understand why.
Can someone please help? I'm working on this problem for over 2 days now...
I don't know exactly what you mean. If you want to trigger an AJAX request while clicking on a link you can do the following in the belonging view of your action. In this example the script will output the AJAX response.
1) Create a ViewHelper containing something like that:
$uriBuilder = $this->controllerContext->getUriBuilder();
$uri = $uriBuilder->uriFor(
'title',
array("param1" => $value1, "param2" => $value2),
'<controllerName>',
'<extKey>',
'<pluginName>');
return '<a id="link" href="'.$uri.'">';
2) Call the ViewHelper in your view template and add a output container div with an id.
3) Implement the JavaScript for AJAX call
function loadurl(dest, obj) {
try {
xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
console.log(e);
}
xmlhttp.onreadystatechange = function() {
triggered(obj);
};
xmlhttp.open("GET", dest);
xmlhttp.send(null);
}
function triggered(obj) {
if ((xmlhttp.readyState == 4) && (xmlhttp.status == 200)) {
document.getElementById(obj).innerHTML = xmlhttp.responseText;
}
}
window.addEventListener("load", function() {
var item = document.getElementsById('link');
item.addEventListener('click', function(event) {
event.preventDefault();
var href = this.childNodes[1].getAttribute("href");
loadurl(href, 'idOfOutputContainer');
}
}
It's not implemented in 6.2 as noted in my bug report: action and controller not used in RequestBuilder.php:loadDefaultValues
For my company I'm working on a custom translation module for Magento.
On the form for translating an existing string, I would like to change the behaviour of the "Save And Continue"-button to a "Save And Next"-button.
With which I mean that instead of still editing thesame string, you get the next one in line.
I have tried to edit the link that is called for the Save And Continue:
Original:
[save-link] + "/back/edit/"
To:
[save-link] + "/back/edit/id/[id]/"
But to no avail. I'm hoping someone can set me in the right direction.
The unchanged code of the edit-form:
<?php
class Phpro_Advancedtranslate_Block_Adminhtml_Edit extends Mage_Adminhtml_Block_Widget_Form_Container
{
public function __construct()
{
parent::__construct();
$this->_objectId = 'id';
$this->_blockGroup = 'advancedtranslate';
$this->_controller = 'adminhtml';
$this->_updateButton('save', 'label', Mage::helper('advancedtranslate')->__('Save Item'));
$this->_updateButton('delete', 'label', Mage::helper('advancedtranslate')->__('Delete Item'));
$this->_addButton('saveandcontinue', array(
'label' => Mage::helper('adminhtml')->__('Save And Next'),
'onclick' => 'saveAndContinueEdit()',
'class' => 'save',
), -100);
$currentId = Mage::getSIngleton('adminhtml/session')->getTranslateId();
$strings = Mage::getModel("advancedtranslate/advancedtranslate")->getCollection();
foreach ($strings as $string) {
$id = $string->getId();
if ($id != $currentId && $id < $nextId) {
$nextId = $id;
}
}
$this->_formScripts[] = "
function toggleEditor() {
if (tinyMCE.getInstanceById('advancedtranslate_content') == null) {
tinyMCE.execCommand('mceAddControl', false, 'advancedtranslate_content');
} else {
tinyMCE.execCommand('mceRemoveControl', false, 'advancedtranslate_content');
}
}
function saveAndContinueEdit(){
editForm.submit($('edit_form').action+'back/edit/');
}
";
}
public function getHeaderText()
{
return Mage::helper('advancedtranslate')->__("Edit Item '%s'", 'test');
}
}
This functionality has to happen in the controller that handles the Post. Set the _redirect to redirect to the next item.