Why aren't :q params passing to the controller?!
Here's the controller code:
def advanced_search
#search = Isbn.where(:client_id => current_user.client_id).search(params[:q])
#search.build_grouping unless #search.groupings.any?
#isbns = params[:distinct].to_i.zero? ? #search.result : #search.result(distinct: true)
respond_with #isbns
end
Here's application_helper with the build_grouping bit it:
def setup_search_form(builder)
fields = builder.grouping_fields builder.object.new_grouping, object_name: 'new_object_name', child_index: "new_grouping" do |f|
render('grouping_fields', f: f)
end
Here's the form:
<%= search_form_for #search, url: advanced_search_isbns_path, html: {method: :post} do |f| %>
<% setup_search_form f %>
#...
<%= render 'results' %>
Here's the trace:
Processing by IsbnsController#advanced_search as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"P074clk3UDe3cNNEG6IZ6TXm2q+fr8dMJfJwSBSx/1c=", "q"=>{"g"=>{"0"=>{"m"=>"or"}}}, "commit"=>"Search"}
Here's the source of search_form_for:
# File 'lib/ransack/helpers/form_helper.rb', line 4
def search_form_for(record, options = {}, &proc)
if record.is_a?(Ransack::Search)
search = record
options[:url] ||= polymorphic_path(search.klass)
elsif record.is_a?(Array) && (search = record.detect {|o| o.is_a?(Ransack::Search)})
options[:url] ||= polymorphic_path(record.map {|o| o.is_a?(Ransack::Search) ? o.klass : o})
else
raise ArgumentError, "No Ransack::Search object was provided to search_form_for!"
end
options[:html] ||= {}
html_options = {
:class => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
:id => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
:method => :get
}
options[:as] ||= 'q'
options[:html].reverse_merge!(html_options)
options[:builder] ||= FormBuilder
form_for(record, options, &proc)
end
Here's the source of search:
# File 'lib/ransack/search.rb', line 16
def initialize(object, params = {}, options = {})
params ||= {}
#context = Context.for(object)
#context.auth_object = options[:auth_object]
#base = Nodes::Grouping.new(#context, 'and')
build(params.with_indifferent_access)
end
In fact, here's the whole chuffing lot.
Plus: all the routes for the model, included for completeness:
resources :isbns, only: :index do
match 'advanced_search' => 'isbns#advanced_search',
on: :collection, via: [:get, :post], as: :advanced_search
end
resources :isbns do
get :profit, :on => :member
get :workings, :on => :member
put :copyisbn, :on => :member
get :onixxml, :on => :member
collection do
post 'edit_multiple'
put 'update_multiple'
get 'onix'
get 'add_subschemes'
get 'xmp'
get 'profitindex'
delete 'destroy_multiple'
end
end
The models file is very busy, so do ask for more, but here are some of the associations:
Isbn.rb:
attr_accessible :marketingtexts_attributes, :prices_attributes #etc
has_many :marketingtexts, :dependent => :destroy
has_many :supplies, :dependent => :destroy
Marketingtext.rb
attr_accessible :user_id, :created_at, :legacycode, :updated_at, :client_id, :isbn_id, #etc
belongs_to :isbn
before_save :map_legacy_codes
validates :client_id, :presence => true
Supply.rb
attr_accessible :user_id, :client_id, :isbn_id, :prices_attributes, #etc
belongs_to :isbn
has_many :supplydetails, :dependent => :destroy
has_many :prices, :through => :supplydetails
accepts_nested_attributes_for :supplydetails
I can run the cloned ransack demo code on my machine. In my own app, I've excluded all the javascript except jquery and the search script. I'm increasingly thinking it's a conflict with another gem or some bit of code.
Related
parent.rb
class Parent
include Mongoid::Document
field :name, type: String
field :hobby, type: String
field :born, type: Date
has_many :children
accepts_nested_attributes_for :children
def self.search(search)
if search
any_of({name: search}, {hobby: search})
end
end
end
child.rb:
class Child
include Mongoid::Document
field :name, type: String
field :hobby, type: String
field :born, type: Date
belongs_to :parent
end
parents_controller.rb
class ParentsController < ApplicationController
before_action :set_parent, only: [:show, :edit, :update, :destroy]
# GET /parents
# GET /parents.json
def index
if params[:search].empty?
#parents = Parent.all
else
#parents = Parent.search(params[:search])
end
end
# GET /parents/1
# GET /parents/1.json
def show
end
# GET /parents/new
def new
#parent = Parent.new
end
# GET /parents/1/edit
def edit
end
# POST /parents
# POST /parents.json
def create
#parent = Parent.new(parent_params)
respond_to do |format|
if #parent.save
format.html { redirect_to #parent, notice: 'Parent was successfully created.' }
format.json { render action: 'show', status: :created, location: #parent }
else
format.html { render action: 'new' }
format.json { render json: #parent.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /parents/1
# PATCH/PUT /parents/1.json
def update
respond_to do |format|
if #parent.update_attributes(parent_params)
format.html { redirect_to #parent, notice: 'Parent was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #parent.errors, status: :unprocessable_entity }
end
end
end
# DELETE /parents/1
# DELETE /parents/1.json
def destroy
#parent.destroy
respond_to do |format|
format.html { redirect_to parents_url }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_parent
#parent = Parent.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def parent_params
params.require(:parent).permit(:name, :hobby, :born)
end
end
search tag in the product's index.html.erb
<%= form_tag parents_path, :method => 'get' do %>
<p>
<%= text_field_tag :search, params[:search] %>
<%= submit_tag "Search", :name => nil %>
</p>
<% end %>
Questions:
When I have 2 names: Alex and Alex Jr., and when I search for
"Alex", it's giving me only one result. But I want "Alex Jr" also to
be as a search result.
My search is case-sensitive. I'd like to ignore this.
I want to be able to search not only in the main document, but also
in the nested ones in all levels. In this case, I have main document as a model parent.rb and nested one called child.rb. So, for example, when I search for name, I will want to search not only parent's name but also in his child.
I would like to add an ability to search by specifying date range.
For example, to find all records who was born from A date to B date.
You can write:
def self.search(search)
if search
any_of({name: /#{search}/i}, {hobby: /#{search}/i})
end
end
That will give you all objects that include this value, with ignoring case-sensitive.
About adding search by date range.
Send to your controller an additional value - for example ~ search_to.
def index
#parents = if params[:search]
Parent.search(params[:search], params[:search_to]) # when searching name/hobby, params[:search_to] will be nil
else
Parent.all
end
end
Your search function:
def self.search(search, search_to)
if search && search_to
where(:born => {'$gte' => Date.parse(search),'$lt' => Date.parse(search_to)})
elsif search
any_of({name: /#{search}/i}, {hobby: /#{search}/i})
end
end
Question 3 - I don't understand what is your problem.
I have a module Search in my ZF2 application. The user fills in a search form out and gets a list of courses.
Now I'm adding the pagination to the module. The paginator is basically working: I can retrieve data over it and the pagination is displayed correctly (pagelinks 1-7 for 70 found courses with the dafault setting 10 items per page).
But it's still not usable. When I click on a pagelink, the form POST data is lost. I know -- it cannot work the way, how I implemented it (see the code below). But I have no idea, how to do it correctly, in order to eep checking the form data and nonetheless be able to use pagination.
That is my code:
Table class Search\Model\CourseTable
class CourseTable {
...
// without pagination
// public function findAllByCriteria(CourseSearchInput $input) {
// with pagination
public function findAllByCriteria(CourseSearchInput $input, $pageNumber) {
...
$select = new Select();
$where = new Where();
$having = new Having();
...
// without pagination
// $resultSet = $this->tableGateway->selectWith($select);
// return $resultSet;
// with pagination
$adapter = new \MyNamespqce\Paginator\Adapter\DbSelect($select, $this->tableGateway->getAdapter());
$paginator = new \Zend\Paginator\Paginator($adapter);
$paginator->setCurrentPageNumber($pageNumber);
return $paginator;
}
...
}
Search\Controller\SearchController
class SearchController extends AbstractActionController {
public function searchCoursesAction() {
$form = $this->getServiceLocator()->get('Search\Form\CourseSearchForm');
$request = $this->getRequest();
if ($request->isPost()) {
$courseSearchInput = new CourseSearchInput();
$form->setInputFilter($courseSearchInput->getInputFilter());
$form->setData($request->getPost());
if ($form->isValid()) {
$courseSearchInput->exchangeArray($form->getData());
// without pagination
// $courses = $this->getCourseTable()->findAllByCriteria($courseSearchInput);
// with pagination
$page = $this->params()->fromRoute('page');
$paginator = $this->getCourseTable()->findAllByCriteria($courseSearchInput, $page);
} else {
$paginator = null;
}
} else {
$paginator = null;
}
return new ViewModel(array(
'form' => $form,
// without pagination
// 'courses' => $courses,
// with pagination
'paginator' => $paginator,
'cities' => ...
));
}
...
}
How to get it working?
I also have the same problem, and I have solved it. But this is not good way. May be the idea will help you.
I solved it as follow: (Search pagination for Zend tutorial album module)
I build two action in controller named "search" and "index".
Whenever the search form submitted, it always post the value to search action. Search action build the url with search parameters, and redirect to index to disply search result.
And when the pagination links clicked, then posted values are passed through url. So whenever index action ask for search parameters, it always get the values in same format.
I defined route as follows:
'album' => array(
'type' => 'segment',
'options' => array(
'route' => '/album[/:action][/:id][/page/:page][/order_by/:order_by][/:order][/search_by/:search_by]',
'constraints' => array(
'action' => '(?!\bpage\b)(?!\border_by\b)(?!\bsearch_by\b)[a-zA-Z][a-zA-Z0-9_-]*',
'id' => '[0-9]+',
'page' => '[0-9]+',
'order_by' => '[a-zA-Z][a-zA-Z0-9_-]*',
'order' => 'ASC|DESC',
),
'defaults' => array(
'controller' => 'Album\Controller\Album',
'action' => 'index',
),
),
),
There is a parameter named "search_by", which will keep all search parameters as a json string. This is the point, which is not good I know, but have not find any other way yet.
"Search" action build this string as -
public function searchAction()
{
$request = $this->getRequest();
$url = 'index';
if ($request->isPost()) {
$formdata = (array) $request->getPost();
$search_data = array();
foreach ($formdata as $key => $value) {
if ($key != 'submit') {
if (!empty($value)) {
$search_data[$key] = $value;
}
}
}
if (!empty($search_data)) {
$search_by = json_encode($search_data);
$url .= '/search_by/' . $search_by;
}
}
$this->redirect()->toUrl($url);
}
And next index action decode the string, do necessary action, and also send the json string to view.
public function indexAction() {
$searchform = new AlbumSearchForm();
$searchform->get('submit')->setValue('Search');
$select = new Select();
$order_by = $this->params()->fromRoute('order_by') ?
$this->params()->fromRoute('order_by') : 'id';
$order = $this->params()->fromRoute('order') ?
$this->params()->fromRoute('order') : Select::ORDER_ASCENDING;
$page = $this->params()->fromRoute('page') ? (int) $this->params()->fromRoute('page') : 1;
$select->order($order_by . ' ' . $order);
$search_by = $this->params()->fromRoute('search_by') ?
$this->params()->fromRoute('search_by') : '';
$where = new \Zend\Db\Sql\Where();
$formdata = array();
if (!empty($search_by)) {
$formdata = (array) json_decode($search_by);
if (!empty($formdata['artist'])) {
$where->addPredicate(
new \Zend\Db\Sql\Predicate\Like('artist', '%' . $formdata['artist'] . '%')
);
}
if (!empty($formdata['title'])) {
$where->addPredicate(
new \Zend\Db\Sql\Predicate\Like('title', '%' . $formdata['title'] . '%')
);
}
}
if (!empty($where)) {
$select->where($where);
}
$album = $this->getAlbumTable()->fetchAll($select);
$totalRecord = $album->count();
$itemsPerPage = 2;
$album->current();
$paginator = new Paginator(new paginatorIterator($album));
$paginator->setCurrentPageNumber($page)
->setItemCountPerPage($itemsPerPage)
->setPageRange(7);
$searchform->setData($formdata);
return new ViewModel(array(
'search_by' => $search_by,
'order_by' => $order_by,
'order' => $order,
'page' => $page,
'paginator' => $paginator,
'pageAction' => 'album',
'form' => $searchform,
'totalRecord' => $totalRecord
));
}
All the sorting and paging url contain that string.
If you know all the searching paarameters before, then you can define that at route, and pass like the same way without json string. As I have to build a common search, I have build a single string.
Source code for "Album search" is available in git hub at https://github.com/tahmina8765/zf2_search_with_pagination_example.
Live Demo: http://zf2pagination.lifencolor.com/public/album
#Sam & #automatix in the question comments are both right. My suggestion (though I'm looking for a simpler alternative) is to construct a segment route, which covers all of the options that you're likely to need and start with a standard form POST request.
Then, after the request is validated, pass the form data to the paginationControl helper as follows:
$resultsView = new ViewModel(array(
'paginator' => $paginator,
'routeParams' => array_filter($form->getData())
));
Then, in your view template, set the route parameters in the paginationControl view helper:
<?php echo $this->paginationControl($paginator, 'Sliding', 'paginator/default',
array('routeParams' => $routeParams)
) ?>
I've used array_filter here because it's a really simple way of removing any element from the form data that's null, empty or so on. That way you don't pass in extra data that you don't need.
I have a problem with searching in Yii. I have two models: Teams and Workers. On website there is a page called 'Team Workers' where I want to display CGridView widget with searching that displays Workers from the team (team id is passed as a _GET parameter).
I did this in TeamsController:
public function actionWorkers($id)
{
$model = Teams::model()->findByPk($id);
$workers = Workers::model();
$workers->unsetAttributes();
if(isset($_GET['Workers']))
{
$_GET['Workers']['idTeam'] = $id;
$workers->attributes = $_GET['Workers'];
}
else {
$workers->attributes = array('idTeam' => $id);
}
$teamWorkers = $workers;
$this->render('workers', array(
'model' => $model,
'teamWorkers' => $teamWorkers
));
}
And in the view file:
<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'team-workers-grid',
'dataProvider'=>$teamWorkers->search(),
'filter' => $teamWorkers,
'columns'=>array(
'name',
'surname',
array(
'id' => 'idWorker',
'class' => 'CCheckBoxColumn',
'checked' => '$data->confirmer',
'selectableRows' => '2',
// 'headerTemplate' => '{item}'
)
),
)); ?>
I got the error:
CDbCommand nie zdołał wykonać instrukcji SQL: SQLSTATE[23000]: Integrity constraint
violation: 1052 Column 'idTeam' in where clause is ambiguous. The SQL statement
executed was: SELECT COUNT(DISTINCT `t`.`idWorker`) FROM `workers` `t` LEFT OUTER JOIN
`teams` `Team` ON (`t`.`idTeam`=`Team`.`idTeam`) WHERE ((idTeam=:ycp0) AND (Team.name
LIKE :ycp1))
When I dont set idTeam attribute - it works fine. It's pretty weird - at the regular CRUD admin page - idTeam attribute is passed and that works fine.
Hot to deal with it?
In Workers::search() you have something like
$criteria->compare('idTeam',$this->idTeam);
Change it to
$criteria->compare('t.idTeam',$this->idTeam);
i.e prefix sql attribute with t. if it is from current model or with relation name if from other table/model
Also instead of:
$workers->attributes = array('idTeam' => $id);
yould could keep it simpler with:
$workers->idTeam = $id;
You have defined the column idTeam in Team and Workers. By joining those tables you would have a duplicate ("ambiguous") column in the result. That's what the error message tells you.
To solve this you have to use an alias for one of the columns.
I want to extend default Drupal 7 node search with one additional field.
I alter search form with the following new field:
function mymodule_form_search_form_alter(&$form, &$form_state, $form_id) {
$form['basic']['site'] = array(
'#type' => 'select',
'#options' => array(
'KEY1' => 'TITLE1',
'KEY2' => 'TITLE2',
'KEY3' => 'TITLE3'
)
);
}
I have a field called field_data_field_site.field_site_value which i need to use as a filter in this search.
I've tried to read about hook_search_* functions but didn't get the idea.
My question is the following. How can I extend search form? Anyone have live examples?
The following is the best way I solve this problem.
First of all I need to alter Drupal's search block and search form with my field and define new submit function.
/**
* Implements hook_form_FORM_ID_alter().
*/
function mymodule_form_search_block_form_alter(&$form, &$form_state, $form_id) {
$form['#submit'][] = 'search_form_alter_submit';
$form['site'] = array(
'#type' => 'select',
'#options' => _options(),
'#default_value' => (($_GET['site']) ? $_GET['site'] : '')
);
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function mymodule_form_search_form_alter(&$form, &$form_state, $form_id) {
$form['#submit'][] = 'search_form_alter_submit';
$form['basic']['site'] = array(
'#type' => 'select',
'#options' => _options(),
'#default_value' => (($_GET['site']) ? $_GET['site'] : '')
);
}
function _options() {
return array(
'' => 'Select site',
'site-1' => 'Site 1',
'site-2' => 'Site 2'
);
}
Submit function will forward us to default search/node page but with our query. Page would look like search/node/Our-query-string?site=Our-option-selected.
function search_form_alter_submit($form, &$form_state) {
$path = $form_state['redirect'];
$options = array(
'query' => array(
'site' => $form_state['values']['site']
)
);
drupal_goto($path, $options);
}
Next step is to use hook_search_info (Don't forget to turn it on and set as default on admin/config/search/settings page).
/**
* Implements hook_search_info().
*/
function mymodule_search_info() {
return array(
'title' => 'Content',
'path' => 'node',
'conditions_callback' => '_conditions_callback',
);
}
Conditions callback function defined in hook_search_info. We need to provide additional queries to our search.
function _conditions_callback($keys) {
$conditions = array();
if (!empty($_REQUEST['site'])) {
$conditions['site'] = $_REQUEST['site'];
}
return $conditions;
}
Finally, hook_search_execute will filter our content by our query. I used default code from this hook with modifications I need.
/**
* Implements hook_search_execute().
*/
function mymodule_search_execute($keys = NULL, $conditions = NULL) {
// Build matching conditions
$query = db_select('search_index', 'i', array('target' => 'slave'))
->extend('SearchQuery')
->extend('PagerDefault');
$query->join('node', 'n', 'n.nid = i.sid');
// Here goes my filter where I joined another table and
// filter by required field
$site = (isset($conditions['site'])) ? $conditions['site'] : NULL;
if ($site) {
$query->leftJoin('field_data_field_site', 's', 's.entity_id = i.sid');
$query->condition('s.field_site_value', $site);
}
// End of my filter
$query
->condition('n.status', 1)
->addTag('node_access')
->searchExpression($keys, 'node');
// Insert special keywords.
$query->setOption('type', 'n.type');
$query->setOption('language', 'n.language');
if ($query->setOption('term', 'ti.tid')) {
$query->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
}
// Only continue if the first pass query matches.
if (!$query->executeFirstPass()) {
return array();
}
// Add the ranking expressions.
_node_rankings($query);
// Load results.
$find = $query
->limit(10)
->execute();
$results = array();
foreach ($find as $item) {
// Build the node body.
$node = node_load($item->sid);
node_build_content($node, 'search_result');
$node->body = drupal_render($node->content);
// Fetch comments for snippet.
$node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node);
// Fetch terms for snippet.
$node->rendered .= ' ' . module_invoke('taxonomy', 'node_update_index', $node);
$extra = module_invoke_all('node_search_result', $node);
$results[] = array(
'link' => url("node/{$item->sid}", array('absolute' => TRUE)),
'type' => check_plain(node_type_get_name($node)),
'title' => $node->title,
'user' => theme('username', array('account' => $node)),
'date' => $node->changed,
'node' => $node,
'extra' => $extra,
'score' => $item->calculated_score,
'snippet' => search_excerpt($keys, $node->body)
);
}
return $results;
}
I'd be happy if anyone would give me a better answer.
I can't for the life of me figure out where the hook_search function in drupal is located. Is it something I need to add to a file to access?
Hook functions don't exist by name -- they indicate a naming convention that can be followed to respond to that particular "hook"...
An example would be the node_search() function. When the search module calls module_invoke_all('search'), all functions named foo_search(), where foo is the name of an enabled module, will be called. The details of the search hook in particular are found on api.drupal.org.
function hook_search($op = 'search', $keys = null) {
switch ($op) {
case 'name':
return t('content');
case 'reset':
variable_del('node_cron_last');
return;
case 'search':
$find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid '. node_access_join_sql() .' INNER JOIN {users} u ON n.uid = u.uid', 'n.status = 1 AND '. node_access_where_sql());
$results = array();
foreach ($find as $item) {
$node = node_load(array('nid' => $item));
$extra = node_invoke_nodeapi($node, 'search result');
$results[] = array('link' => url('node/'. $item),
'type' => node_invoke($node, 'node_name'),
'title' => $node->title,
'user' => theme('username', $node),
'date' => $node->changed,
'extra' => $extra,
'snippet' => search_excerpt($keys, check_output($node->body, $node->format)));
}
return $results;
}
}