Refactoring: A nicer and easier syntax for searching models (#5841)

* Adds the ability to search by dates

Adding extra „where“-conditions to the „TextSearch“ queries, allowing the users to search by dates

* Adds missing dates to $dates in models

* Removes duplicated „where“ conditions

* Adds the Searchable trait to models, defining the searchable attributes and relations

* Removes the old text search methods

* Adds back additional conditions to the search

These conditions could not be modeled in the „attributes“ or „relations“, so we include them here

* Removes unnecessary check for the deleted_at attribute

* Fixes typo in comments

* suppresses errors from Codacy

We can safely ignore the error codacy is throwing here, since this method is a standin/noop for models who need to implement more advanced searches
This commit is contained in:
Till Deeke 2018-07-16 23:13:07 +02:00 committed by snipe
parent 240e642fe9
commit baa3be728d
23 changed files with 679 additions and 539 deletions

View file

@ -24,7 +24,7 @@ class AccessoriesController extends Controller
$this->authorize('view', Accessory::class);
$allowed_columns = ['id','name','model_number','eol','notes','created_at','min_amt','company_id'];
$accessories = Accessory::whereNull('accessories.deleted_at')->with('category', 'company', 'manufacturer', 'users', 'location');
$accessories = Accessory::with('category', 'company', 'manufacturer', 'users', 'location');
if ($request->has('search')) {
$accessories = $accessories->TextSearch($request->input('search'));

View file

@ -24,7 +24,7 @@ class ComponentsController extends Controller
public function index(Request $request)
{
$this->authorize('view', Component::class);
$components = Company::scopeCompanyables(Component::select('components.*')->whereNull('components.deleted_at')
$components = Company::scopeCompanyables(Component::select('components.*')
->with('company', 'location', 'category'));
if ($request->has('search')) {

View file

@ -24,7 +24,6 @@ class ConsumablesController extends Controller
$this->authorize('index', Consumable::class);
$consumables = Company::scopeCompanyables(
Consumable::select('consumables.*')
->whereNull('consumables.deleted_at')
->with('company', 'location', 'category', 'users', 'manufacturer')
);

View file

@ -26,7 +26,7 @@ class SuppliersController extends Controller
$suppliers = Supplier::select(
array('id','name','address','address2','city','state','country','fax', 'phone','email','contact','created_at','updated_at','deleted_at','image','notes')
)->withCount('assets')->withCount('licenses')->withCount('accessories')->whereNull('deleted_at');
)->withCount('assets')->withCount('licenses')->withCount('accessories');
if ($request->has('search')) {

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -25,6 +26,28 @@ class Accessory extends SnipeModel
'requestable' => 'boolean'
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'order_number', 'purchase_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'category' => ['name'],
'company' => ['name'],
'manufacturer' => ['name'],
'supplier' => ['name'],
'location' => ['name']
];
/**
* Set static properties to determine which checkout/checkin handlers we should use
*/
@ -171,40 +194,6 @@ class Accessory extends SnipeModel
return $remaining;
}
/**
* Query builder scope to search on text
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('location', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('accessories.name', 'LIKE', '%'.$search.'%')
->orWhere('accessories.model_number', 'LIKE', '%'.$search.'%')
->orWhere('accessories.order_number', 'LIKE', '%'.$search.'%');
});
}
/**
* Query builder scope to order on company
*

View file

@ -1,5 +1,6 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -24,6 +25,24 @@ class Actionlog extends SnipeModel
public $timestamps = true;
protected $fillable = [ 'created_at', 'item_type','user_id','item_id','action_type','note','target_id', 'target_type' ];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['action_type', 'note', 'log_meta'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'company' => ['name']
];
// Overridden from Builder to automatically add the company
public static function boot()
{
@ -200,31 +219,4 @@ class Actionlog extends SnipeModel
->orderBy('created_at', 'asc')
->get();
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' OR ', $search);
return $query->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->where(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('action_type', 'LIKE', '%'.$search.'%')
->orWhere('note', 'LIKE', '%'.$search.'%')
->orWhere('log_meta', 'LIKE', '%'.$search.'%');
}
});
}
}

View file

@ -4,11 +4,13 @@ namespace App\Models;
use App\Exceptions\CheckoutNotAllowed;
use App\Http\Traits\UniqueSerialTrait;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use AssetPresenter;
use Auth;
use Carbon\Carbon;
use Config;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Log;
use Watson\Validating\ValidatingTrait;
@ -111,7 +113,42 @@ class Asset extends Depreciable
'warranty_months',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = [
'name',
'asset_tag',
'serial',
'order_number',
'purchase_cost',
'notes',
'created_at',
'updated_at',
'purchase_date',
'expected_checkin',
'next_audit_date',
'last_audit_date'
];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'assetstatus' => ['name'],
'supplier' => ['name'],
'company' => ['name'],
'defaultLoc' => ['name'],
'model' => ['name', 'model_number'],
'model.category' => ['name'],
'model.manufacturer' => ['name'],
];
public function getDisplayNameAttribute()
{
@ -580,6 +617,49 @@ class Asset extends Depreciable
}
}
/**
* Run additional, advanced searches.
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term The search term
* @return Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, string $term) {
/**
* Assigned user
*/
$query = $query->leftJoin('users as assets_users',function ($leftJoin) {
$leftJoin->on("assets_users.id", "=", "assets.assigned_to")
->where("assets.assigned_type", "=", User::class);
});
$query = $query
->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.username', 'LIKE', '%'.$term.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$term%", "%$term%"]);
/**
* Assigned location
*/
$query = $query->leftJoin('locations as assets_locations',function ($leftJoin) {
$leftJoin->on("assets_locations.id","=","assets.assigned_to")
->where("assets.assigned_type","=",Location::class);
});
$query = $query->orWhere('assets_locations.name', 'LIKE', '%'.$term.'%');
/**
* Assigned assets
*/
$query = $query->leftJoin('assets as assigned_assets',function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Asset::class);
});
$query = $query->orWhere('assigned_assets.name', 'LIKE', '%'.$term.'%');
return $query;
}
/**
* -----------------------------------------------
@ -813,83 +893,6 @@ class Asset extends Depreciable
return $query->where("accepted", "=", "accepted");
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API.
* This is really horrible, but I can't think of a less-awful way to do it.
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' OR ', $search);
return $query->leftJoin('users as assets_users',function ($leftJoin) {
$leftJoin->on("assets_users.id", "=", "assets.assigned_to")
->where("assets.assigned_type", "=", User::class);
})->leftJoin('locations as assets_locations',function ($leftJoin) {
$leftJoin->on("assets_locations.id","=","assets.assigned_to")
->where("assets.assigned_type","=",Location::class);
})->leftJoin('assets as assigned_assets',function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Asset::class);
})->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->whereHas('model', function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%')
->orWhere('models.name', 'LIKE', '%'.$search.'%')
->orWhere('models.model_number', 'LIKE', '%'.$search.'%');
});
});
})->orWhereHas('model', function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('assetstatus', function ($query) use ($search) {
$query->where('status_labels.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('supplier', function ($query) use ($search) {
$query->where('suppliers.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%' . $search . '%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('defaultLoc', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->where('assets_users.first_name', 'LIKE', '%'.$search.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$search%", "%$search%"])
->orWhere('assets_users.username', 'LIKE', '%'.$search.'%')
->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%')
->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%');
})->orWhere('assets.name', 'LIKE', '%'.$search.'%')
->orWhere('assets.asset_tag', 'LIKE', '%'.$search.'%')
->orWhere('assets.serial', 'LIKE', '%'.$search.'%')
->orWhere('assets.order_number', 'LIKE', '%'.$search.'%')
->orWhere('assets.purchase_cost', 'LIKE', '%'.$search.'%')
->orWhere('assets.notes', 'LIKE', '%'.$search.'%');
}
foreach (CustomField::all() as $field) {
$query->orWhere('assets.'.$field->db_column_name(), 'LIKE', "%$search%");
}
})->withTrashed()->whereNull("assets.deleted_at"); //workaround for laravel bug
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API.
*

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Lang;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -19,7 +20,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
use ValidatingTrait;
protected $dates = [ 'deleted_at' ];
protected $dates = [ 'deleted_at', 'start_date' , 'completion_date'];
protected $table = 'asset_maintenances';
// Declaring rules for form validation
protected $rules = [
@ -34,6 +35,23 @@ class AssetMaintenance extends Model implements ICompanyableChild
'cost' => 'numeric|nullable'
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function getCompanyableParents()
{
return [ 'asset' ];
@ -139,30 +157,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
**/
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('asset_maintenances.title', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.notes', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.asset_maintenance_type', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.cost', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.start_date', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.completion_date', 'LIKE', '%'.$search.'%');
});
}
/**
* Query builder scope to order on admin user

View file

@ -3,6 +3,7 @@ namespace App\Models;
use App\Models\Requestable;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -68,6 +69,26 @@ class AssetModel extends SnipeModel
'user_id',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'depreciation' => ['name'],
'category' => ['name'],
'manufacturer' => ['name'],
];
public function assets()
{
return $this->hasMany('\App\Models\Asset', 'model_id');
@ -160,38 +181,7 @@ class AssetModel extends SnipeModel
{
return $query->where('requestable', '1');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where('models.name', 'LIKE', "%$search%")
->orWhere('model_number', 'LIKE', "%$search%")
->orWhere(function ($query) use ($search) {
$query->whereHas('depreciation', function ($query) use ($search) {
$query->where('depreciations.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
});
}
}
/**
* Query builder scope to search on text, including catgeory and manufacturer name

View file

@ -3,6 +3,7 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -63,6 +64,21 @@ class Category extends SnipeModel
'user_id',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'category_type'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function has_models()
{
@ -143,22 +159,4 @@ class Category extends SnipeModel
return $query->where('require_acceptance', '=', true);
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%')
->orWhere('category_type', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Auth;
use DB;
@ -35,6 +36,21 @@ final class Company extends SnipeModel
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'created_at', 'updated_at'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
/**
* The attributes that are mass assignable.
@ -192,20 +208,4 @@ final class Company extends SnipeModel
{
return $this->hasMany(Component::class, 'company_id');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -67,6 +68,26 @@ class Component extends SnipeModel
'serial',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'order_number', 'serial', 'purchase_cost', 'purchase_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'category' => ['name'],
'company' => ['name'],
'location' => ['name'],
];
public function location()
{
return $this->belongsTo('\App\Models\Location', 'location_id');
@ -114,49 +135,7 @@ class Component extends SnipeModel
$total = $this->qty;
$remaining = $total - $checkedout;
return $remaining;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' ', $search);
return $query->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('location', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('components.name', 'LIKE', '%'.$search.'%')
->orWhere('components.order_number', 'LIKE', '%'.$search.'%')
->orWhere('components.serial', 'LIKE', '%'.$search.'%')
->orWhere('components.purchase_cost', 'LIKE', '%'.$search.'%');
}
});
}
}
/**
* Query builder scope to order on company

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -68,6 +69,27 @@ class Consumable extends SnipeModel
'requestable'
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'order_number', 'purchase_cost', 'purchase_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'category' => ['name'],
'company' => ['name'],
'location' => ['name'],
'manufacturer' => ['name'],
];
public function setRequestableAttribute($value)
{
if ($value == '') {
@ -163,50 +185,6 @@ class Consumable extends SnipeModel
return $remaining;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' ', $search);
return $query->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('location', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('consumables.name', 'LIKE', '%'.$search.'%')
->orWhere('consumables.order_number', 'LIKE', '%'.$search.'%')
->orWhere('consumables.purchase_cost', 'LIKE', '%'.$search.'%');
}
});
}
/**
* Query builder scope to order on company
*

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Model;
use Log;
use Watson\Validating\ValidatingTrait;
@ -45,6 +46,22 @@ class Department extends SnipeModel
'notes',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'notes'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function company()
{
@ -76,23 +93,7 @@ class Department extends SnipeModel
{
return $this->belongsTo('\App\Models\Location', 'location_id');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextsearch($query, $search)
{
return $query->where('name', 'LIKE', "%$search%")
->orWhere('notes', 'LIKE', "%$search%");
}
/**
* Query builder scope to order on location name
*

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Watson\Validating\ValidatingTrait;
@ -31,6 +32,21 @@ class Depreciation extends SnipeModel
*/
protected $fillable = ['name','months'];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'months'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function has_models()
@ -41,23 +57,5 @@ class Depreciation extends SnipeModel
public function has_licenses()
{
return $this->hasMany('\App\Models\License', 'depreciation_id')->count();
}
/**
* Query builder scope to search on text
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%')
->orWhere('months', 'LIKE', '%'.$search.'%');
});
}
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use Watson\Validating\ValidatingTrait;
class Group extends SnipeModel
@ -22,6 +23,21 @@ class Group extends SnipeModel
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'created_at'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
/**
* Get user groups
@ -36,21 +52,4 @@ class Group extends SnipeModel
{
return json_decode($this->permissions, true);
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -5,6 +5,7 @@ use App\Models\Actionlog;
use App\Models\Company;
use App\Models\LicenseSeat;
use App\Models\Loggable;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Carbon\Carbon;
use DB;
@ -35,7 +36,9 @@ class License extends Depreciable
'created_at',
'updated_at',
'deleted_at',
'purchase_date'
'purchase_date',
'expiration_date',
'termination_date'
];
@ -81,6 +84,34 @@ class License extends Depreciable
'user_id',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = [
'name',
'serial',
'notes',
'order_number',
'purchase_order',
'purchase_cost',
'purchase_date',
'expiration_date',
];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'manufacturer' => ['name'],
'company' => ['name'],
];
public static function boot()
{
parent::boot();
@ -414,39 +445,6 @@ class License extends Depreciable
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('licenses.name', 'LIKE', '%'.$search.'%')
->orWhere('licenses.serial', 'LIKE', '%'.$search.'%')
->orWhere('licenses.notes', 'LIKE', '%'.$search.'%')
->orWhere('licenses.order_number', 'LIKE', '%'.$search.'%')
->orWhere('licenses.purchase_order', 'LIKE', '%'.$search.'%')
->orWhere('licenses.purchase_date', 'LIKE', '%'.$search.'%')
->orWhere('licenses.purchase_cost', 'LIKE', '%'.$search.'%')
->orWhereHas('manufacturer', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
})
->orWhereHas('company', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
});
});
}
/**
* Query builder scope to order on manufacturer
*

View file

@ -4,6 +4,7 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Asset;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Models\User;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
@ -60,6 +61,24 @@ class Location extends SnipeModel
];
protected $hidden = ['user_id'];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'address', 'city', 'state', 'zip', 'created_at'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'parent' => ['name']
];
public function users()
{
return $this->hasMany('\App\Models\User', 'location_id');
@ -171,39 +190,6 @@ class Location extends SnipeModel
return $location_options;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextsearch($query, $search)
{
return $query->where('name', 'LIKE', "%$search%")
->orWhere('address', 'LIKE', "%$search%")
->orWhere('city', 'LIKE', "%$search%")
->orWhere('state', 'LIKE', "%$search%")
->orWhere('zip', 'LIKE', "%$search%")
// This doesn't actually work - need to use a table alias maybe?
->orWhere(function ($query) use ($search) {
$query->whereHas('parent', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
})
// Ugly, ugly code because Laravel sucks at self-joins
->orWhere(function ($query) use ($search) {
$query->whereRaw("parent_id IN (select id from ".DB::getTablePrefix()."locations where name LIKE '%".$search."%') ");
});
});
}
/**
* Query builder scope to order on parent
*

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -47,6 +48,22 @@ class Manufacturer extends SnipeModel
'url',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'created_at'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function has_models()
@ -78,21 +95,4 @@ class Manufacturer extends SnipeModel
{
return $this->hasMany('\App\Models\Consumable', 'manufacturer_id');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -3,6 +3,7 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -35,6 +36,22 @@ class Statuslabel extends SnipeModel
'pending',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
/**
* Get assets with associated status label
@ -108,21 +125,4 @@ class Statuslabel extends SnipeModel
return $statustype;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -3,6 +3,7 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@ -40,6 +41,23 @@ class Supplier extends SnipeModel
use ValidatingTrait;
use UniqueUndeletedTrait;
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
/**
* The attributes that are mass assignable.
*
@ -104,21 +122,4 @@ class Supplier extends SnipeModel
}
return $url;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View file

@ -0,0 +1,238 @@
<?php
namespace App\Models\Traits;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Database\Eloquent\Builder;
/**
* This trait allows for cleaner searching of models,
* moving from complex queries to an easier declarative syntax.
*
* @author Till Deeke <kontakt@tilldeeke.de>
*/
trait Searchable {
/**
* Performs a search on the model, using the provided search terms
*
* @param Illuminate\Database\Eloquent\Builder $query The query to start the search on
* @param string $search
* @return Illuminate\Database\Eloquent\Builder A query with added "where" clauses
*/
public function scopeTextSearch($query, $search)
{
$terms = $this->prepeareSearchTerms($search);
foreach($terms as $term) {
/**
* Search the attributes of this model
*/
$query = $this->searchAttributes($query, $term);
/**
* Search through the custom fields of the model
*/
$query = $this->searchCustomFields($query, $term);
/**
* Search through the relations of the model
*/
$query = $this->searchRelations($query, $term);
/**
* Search for additional attributes defined by the model
*/
$query = $this->advancedTextSearch($query, $term);
}
return $query;
}
/**
* Prepares the search term, splitting and cleaning it up
* @param string $search The search term
* @return array An array of search terms
*/
private function prepeareSearchTerms(string $search) {
return explode(' OR ', $search);
}
/**
* Searches the models attributes for the search term
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term
* @return Illuminate\Database\Eloquent\Builder
*/
private function searchAttributes(Builder $query, string $term) {
foreach($this->getSearchableAttributes() as $column) {
/**
* Making sure to only search in date columns if the search term consists of characters that can make up a MySQL timestamp!
*
* @see https://github.com/snipe/snipe-it/issues/4590
*/
if (!preg_match('/^[0-9 :-]++$/', $term) && in_array($column, $this->getDates())) {
continue;
}
$table = $this->getTable();
$query = $query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%');
}
return $query;
}
/**
* Searches the models custom fields for the search term
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term
* @return Illuminate\Database\Eloquent\Builder
*/
private function searchCustomFields(Builder $query, string $term) {
/**
* If we are searching on something other that an asset, skip custom fields.
*/
if (! $this instanceof Asset) {
return $query;
}
foreach (CustomField::all() as $field) {
$query->orWhere($this->getTable() . '.'. $field->db_column_name(), 'LIKE', '%'.$term.'%');
}
return $query;
}
/**
* Searches the models relations for the search term
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term
* @return Illuminate\Database\Eloquent\Builder
*/
private function searchRelations(Builder $query, string $term) {
foreach($this->getSearchableRelations() as $relation => $columns) {
/**
* Make the columns into a collection,
* for easier handling further down
*
* @var Illuminate\Support\Collection
*/
$columns = collect($columns);
$query = $query->orWhereHas($relation, function($query) use ($relation, $columns, $term) {
$table = $this->getRelationTable($relation);
/**
* We need to form the query properly, starting with a "where",
* otherwise the generated nested select is wrong.
*
* We can just choose the last column, since they all get "and where"d in the end.
* (And because using pop saves us from handling the removal of the first element)
*/
$last = $columns->pop();
$query->where($table . '.' . $last, 'LIKE', '%'.$term.'%');
foreach($columns as $column) {
$query->orWhere($table . '.' . $column, 'LIKE', '%'.$term.'%');
}
});
}
return $query;
}
/**
* Run additional, advanced searches that can't be done using the attributes or relations.
*
* This is a noop in this trait, but can be overridden in the implementing model, to allow more advanced searches
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term The search term
* @return Illuminate\Database\Eloquent\Builder
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function advancedTextSearch(Builder $query, string $term) {
return $query;
}
/**
* Get the searchable attributes, if defined. Otherwise it returns an empty array
*
* @return array The attributes to search in
*/
private function getSearchableAttributes() {
return isset($this->searchableAttributes) ? $this->searchableAttributes : [];
}
/**
* Get the searchable relations, if defined. Otherwise it returns an empty array
*
* @return array The relations to search in
*/
private function getSearchableRelations() {
return isset($this->searchableRelations) ? $this->searchableRelations : [];
}
/**
* Get the table name of a relation.
*
* This method loops over a relation name,
* getting the table name of the last relation in the series.
* So "category" would get the table name for the Category model,
* "model.manufacturer" would get the tablename for the Manufacturer model.
*
* @param string $relation
* @return string The table name
*/
private function getRelationTable($relation) {
$related = $this;
foreach(explode('.', $relation) as $relationName) {
$related = $related->{$relationName}()->getRelated();
}
/**
* Are we referencing the model that called?
* Then get the internal join-tablename, since laravel
* has trouble selecting the correct one in this type of
* parent-child self-join.
*
* @todo Does this work with deeply nested resources? Like "category.assets.model.category" or something like that?
*/
if ($this instanceof $related) {
/**
* Since laravel increases the counter on the hash on retrieval, we have to count it down again.
*
* This causes side effects! Every time we access this method, laravel increases the counter!
*
* Format: laravel_reserved_XXX
*/
$relationCountHash = $this->{$relationName}()->getRelationCountHash();
$parts = collect(explode('_', $relationCountHash));
$counter = $parts->pop();
$parts->push($counter - 1);
return implode('_', $parts->toArray());
}
return $related->getTable();
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
@ -9,6 +10,7 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Http\Traits\UniqueUndeletedTrait;
use Illuminate\Notifications\Notifiable;
@ -68,6 +70,35 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo
'locale' => 'max:10|nullable',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = [
'first_name',
'last_name',
'email',
'username',
'notes',
'phone',
'jobtitle',
'employee_num'
];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'userloc' => ['name'],
'department' => ['name'],
'groups' => ['name'],
'manager' => ['first_name', 'last_name', 'username']
];
public function hasAccess($section)
{
@ -405,6 +436,19 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo
return json_decode($this->permissions, true);
}
/**
* Run additional, advanced searches.
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param string $term The search term
* @return Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, string $term) {
$query = $query->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%$term%", "%$term%"]);
return $query;
}
public function scopeByGroup($query, $id) {
return $query->whereHas('groups', function ($query) use ($id) {
@ -412,55 +456,6 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo
});
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextsearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('users.first_name', 'LIKE', "%$search%")
->orWhere('users.last_name', 'LIKE', "%$search%")
->orWhere('users.email', 'LIKE', "%$search%")
->orWhere('users.username', 'LIKE', "%$search%")
->orWhere('users.notes', 'LIKE', "%$search%")
->orWhere('users.phone', 'LIKE', "%$search%")
->orWhere('users.jobtitle', 'LIKE', "%$search%")
->orWhere('users.employee_num', 'LIKE', "%$search%")
->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%$search%", "%$search%"])
->orWhere(function ($query) use ($search) {
$query->whereHas('userloc', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('department', function ($query) use ($search) {
$query->where('departments.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('groups', function ($query) use ($search) {
$query->where('groups.name', 'LIKE', '%'.$search.'%');
});
})
//Ugly, ugly code because Laravel sucks at self-joins
->orWhere(function ($query) use ($search) {
$query->whereRaw(DB::getTablePrefix()."users.manager_id IN (select id from ".DB::getTablePrefix()."users where first_name LIKE ? OR last_name LIKE ?)", ["%$search%", "%$search%"]);
});
});
}
/**
* Query builder scope for Deleted users
*