Merge pull request #13800 from snipe/fixes/bulk_edit_assets

Fixed FD-38641 - Bulk asset edit unable to update model_id, misc other bugs
This commit is contained in:
snipe 2023-10-26 15:56:47 +01:00 committed by GitHub
commit aab7eb4a85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 215 additions and 73 deletions

View file

@ -112,7 +112,8 @@ class BulkAssetsController extends Controller
public function update(Request $request) public function update(Request $request)
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
$error_bag = []; $has_errors = 0;
$error_array = array();
// Get the back url from the session and then destroy the session // Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index'); $bulk_back_url = route('hardware.index');
@ -120,10 +121,9 @@ class BulkAssetsController extends Controller
$bulk_back_url = $request->session()->pull('bulk_back_url'); $bulk_back_url = $request->session()->pull('bulk_back_url');
} }
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); if (Session::exists('ids')) {
if(Session::exists('ids')) {
$assets = Session::get('ids'); $assets = Session::get('ids');
} elseif (! $request->filled('ids') || count($request->input('ids')) <= 0) { } elseif (! $request->filled('ids') || count($request->input('ids')) <= 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected')); return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@ -160,7 +160,6 @@ class BulkAssetsController extends Controller
$this->conditionallyAddItem('purchase_date') $this->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin') ->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('model_id')
->conditionallyAddItem('order_number') ->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable') ->conditionallyAddItem('requestable')
->conditionallyAddItem('status_id') ->conditionallyAddItem('status_id')
@ -187,6 +186,7 @@ class BulkAssetsController extends Controller
$this->update_array['purchase_cost'] = $request->input('purchase_cost'); $this->update_array['purchase_cost'] = $request->input('purchase_cost');
} }
if ($request->filled('company_id')) { if ($request->filled('company_id')) {
$this->update_array['company_id'] = $request->input('company_id'); $this->update_array['company_id'] = $request->input('company_id');
if ($request->input('company_id') == 'clear') { if ($request->input('company_id') == 'clear') {
@ -208,61 +208,95 @@ class BulkAssetsController extends Controller
} }
$changed = []; $changed = [];
$assetCollection = Asset::where('id' ,$assetId)->get(); $asset = Asset::find($assetId);
foreach ($this->update_array as $key => $value) { foreach ($this->update_array as $key => $value) {
if ($this->update_array[$key] != $assetCollection->toArray()[0][$key]) { if ($this->update_array[$key] != $asset->{$key}) {
$changed[$key]['old'] = $assetCollection->toArray()[0][$key]; $changed[$key]['old'] = $asset->{$key};
$changed[$key]['new'] = $this->update_array[$key]; $changed[$key]['new'] = $this->update_array[$key];
} }
} }
$logAction = new Actionlog(); if ($custom_fields_present) {
$logAction->item_type = Asset::class;
$logAction->item_id = $assetId; $model = $asset->model()->first();
$logAction->created_at = date("Y-m-d H:i:s");
$logAction->user_id = Auth::id(); // Use the rules of the new model fieldsets if the model changed
$logAction->log_meta = json_encode($changed); if ($request->filled('model_id')) {
$logAction->logaction('update'); $this->update_array['model_id'] = $request->input('model_id');
$model = \App\Models\AssetModel::find($request->input('model_id'));
}
// Make sure this model is valid
$assetCustomFields = ($model) ? $model->fieldset : null;
if ($assetCustomFields && $assetCustomFields->fields) {
if($custom_fields_present) {
$asset = Asset::find($assetId);
$assetCustomFields = $asset->model()->first()->fieldset;
if($assetCustomFields && $assetCustomFields->fields) {
foreach ($assetCustomFields->fields as $field) { foreach ($assetCustomFields->fields as $field) {
if (array_key_exists($field->db_column, $this->update_array)) {
$asset->{$field->db_column} = $this->update_array[$field->db_column]; if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted=='1')) {
$saved = $asset->save(); $decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
if(!$saved) {
$error_bag[] = $asset->getErrors(); /*
} * Check if the decrypted existing value is different from one we just submitted
continue; * and if not, pull it out of the object since it shouldn't really be updating at all.
} else { * If we don't do this, it will try to re-encrypt it, and the same value encrypted two
$array = $this->update_array; * different times will have different values, so it will *look* like it was updated
array_except($array, $field->db_column); * but it wasn't.
$asset->save($array); */
if ($decrypted_old != $this->update_array[$field->db_column]) {
$asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]);
} else {
/*
* Remove the encrypted custom field from the update_array, since nothing changed
*/
unset($this->update_array[$field->db_column]);
unset($asset->{$field->db_column});
}
/*
* These custom fields aren't encrypted, just carry on as usual
*/
} else {
if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) {
// Check if this is an array, and if so, flatten it
if (is_array($this->update_array[$field->db_column])) {
$asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]);
} else {
$asset->{$field->db_column} = $this->update_array[$field->db_column];
}
} }
if (!$asset->save()) {
$error_bag[] = $asset->getErrors();
} }
} // endforeach
} // end custom field check
} // end custom fields handler
// Check if it passes validation, and then try to save
if (!$asset->update($this->update_array)) {
// Build the error array
foreach ($asset->getErrors()->toArray() as $key => $message) {
for ($x = 0; $x < count($message); $x++) {
$error_array[$key][] = trans('general.asset') . ' ' . $asset->id . ': ' . $message[$x];
$has_errors++;
}
} }
}
} else { } // end if saved
Asset::find($assetId)->update($this->update_array);
} } // end asset foreach
if ($has_errors > 0) {
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
} }
if(!empty($error_bag)) {
$errors = [];
//find the customfield name from the name of the messagebag items
foreach ($error_bag as $key => $bag) {
foreach($bag->keys() as $key => $value) {
CustomField::where('db_column', $value)->get()->map(function($item) use (&$errors) {
$errors[] = $item->name;
});
}
}
return redirect($bulk_back_url)->with('bulk_errors', array_unique($errors));
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success')); return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
} }
// no values given, nothing to update // no values given, nothing to update

View file

@ -3,6 +3,7 @@ namespace App\Http\Transformers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CustomField; use App\Models\CustomField;
use App\Models\Setting; use App\Models\Setting;
use App\Models\Company; use App\Models\Company;
@ -12,6 +13,7 @@ use App\Models\AssetModel;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
class ActionlogsTransformer class ActionlogsTransformer
{ {
@ -98,6 +100,13 @@ class ActionlogsTransformer
\Log::debug('custom fields do not match'); \Log::debug('custom fields do not match');
$clean_meta[$fieldname]['old'] = "************"; $clean_meta[$fieldname]['old'] = "************";
$clean_meta[$fieldname]['new'] = "************"; $clean_meta[$fieldname]['new'] = "************";
// Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old): '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new): '';
}
} }

View file

@ -11,7 +11,7 @@ use Carbon\Carbon;
class AssetObserver class AssetObserver
{ {
/** /**
* Listen to the User created event. * Listen to the Asset updating event. This fires automatically every time an existing asset is saved.
* *
* @param Asset $asset * @param Asset $asset
* @return void * @return void

View file

@ -38,7 +38,7 @@ class ActionlogFactory extends Factory
{ {
return $this->state(function () { return $this->state(function () {
$target = User::inRandomOrder()->first(); $target = User::inRandomOrder()->first();
$asset = Asset::RTD()->inRandomOrder()->first(); $asset = Asset::inRandomOrder()->RTD()->first();
$asset->update( $asset->update(
[ [

View file

@ -26,6 +26,7 @@ class CustomFieldFactory extends Factory
'format' => '', 'format' => '',
'element' => 'text', 'element' => 'text',
'auto_add_to_fieldsets' => '0', 'auto_add_to_fieldsets' => '0',
'show_in_requestable_list' => '0',
]; ];
} }
@ -66,6 +67,7 @@ class CustomFieldFactory extends Factory
return [ return [
'name' => 'CPU', 'name' => 'CPU',
'help_text' => 'The speed of the processor on this device.', 'help_text' => 'The speed of the processor on this device.',
'show_in_requestable_list' => '1',
]; ];
}); });
} }
@ -79,4 +81,37 @@ class CustomFieldFactory extends Factory
]; ];
}); });
} }
public function testEncrypted()
{
return $this->state(function () {
return [
'name' => 'Test Encrypted',
'field_encrypted' => '1',
'help_text' => 'This is a sample encrypted field.',
];
});
}
public function testCheckbox()
{
return $this->state(function () {
return [
'name' => 'Test Checkbox',
'help_text' => 'This is a sample checkbox.',
'field_values' => "One\nTwo\nThree",
'element' => 'checkbox',
];
});
}
public function testRequired()
{
return $this->state(function () {
return [
'name' => 'Test Required',
'help_text' => 'This is a sample required field.',
];
});
}
} }

View file

@ -33,6 +33,10 @@ class CustomFieldSeeder extends Seeder
CustomField::factory()->count(1)->ram()->create(); CustomField::factory()->count(1)->ram()->create();
CustomField::factory()->count(1)->cpu()->create(); CustomField::factory()->count(1)->cpu()->create();
CustomField::factory()->count(1)->macAddress()->create(); CustomField::factory()->count(1)->macAddress()->create();
CustomField::factory()->count(1)->testEncrypted()->create();
CustomField::factory()->count(1)->testCheckbox()->create();
CustomField::factory()->count(1)->testRequired()->create();
DB::table('custom_field_custom_fieldset')->insert([ DB::table('custom_field_custom_fieldset')->insert([
[ [
@ -66,6 +70,40 @@ class CustomFieldSeeder extends Seeder
'required' => 0, 'required' => 0,
], ],
[
'custom_field_id' => '6',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '6',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '7',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '7',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '8',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 1,
],
]); ]);
} }
} }

View file

@ -38,12 +38,13 @@ class DatabaseSeeder extends Seeder
$this->call(DepreciationSeeder::class); $this->call(DepreciationSeeder::class);
$this->call(StatuslabelSeeder::class); $this->call(StatuslabelSeeder::class);
$this->call(AccessorySeeder::class); $this->call(AccessorySeeder::class);
$this->call(CustomFieldSeeder::class);
$this->call(AssetSeeder::class); $this->call(AssetSeeder::class);
$this->call(LicenseSeeder::class); $this->call(LicenseSeeder::class);
$this->call(ComponentSeeder::class); $this->call(ComponentSeeder::class);
$this->call(ConsumableSeeder::class); $this->call(ConsumableSeeder::class);
$this->call(ActionlogSeeder::class); $this->call(ActionlogSeeder::class);
$this->call(CustomFieldSeeder::class);
Artisan::call('snipeit:sync-asset-locations', ['--output' => 'all']); Artisan::call('snipeit:sync-asset-locations', ['--output' => 'all']);
$output = Artisan::output(); $output = Artisan::output();

View file

@ -368,7 +368,7 @@ return [
'consumables_count' => 'Consumables Count', 'consumables_count' => 'Consumables Count',
'components_count' => 'Components Count', 'components_count' => 'Components Count',
'licenses_count' => 'Licenses Count', 'licenses_count' => 'Licenses Count',
'notification_error' => 'Error:', 'notification_error' => 'Error',
'notification_error_hint' => 'Please check the form below for errors', 'notification_error_hint' => 'Please check the form below for errors',
'notification_bulk_error_hint' => 'The following fields had validation errors and were not edited:', 'notification_bulk_error_hint' => 'The following fields had validation errors and were not edited:',
'notification_success' => 'Success', 'notification_success' => 'Success',

View file

@ -151,6 +151,7 @@
<th data-visible="false" data-sortable="true" class="text-center"><i class="fa fa-eye" aria-hidden="true"><span class="sr-only">Visible to User</span></i></th> <th data-visible="false" data-sortable="true" class="text-center"><i class="fa fa-eye" aria-hidden="true"><span class="sr-only">Visible to User</span></i></th>
<th data-sortable="true" data-searchable="true" class="text-center"><i class="fa fa-envelope" aria-hidden="true"><span class="sr-only">{{ trans('admin/custom_fields/general.show_in_email_short') }}</span></i></th> <th data-sortable="true" data-searchable="true" class="text-center"><i class="fa fa-envelope" aria-hidden="true"><span class="sr-only">{{ trans('admin/custom_fields/general.show_in_email_short') }}</span></i></th>
<th data-sortable="true" data-searchable="true" class="text-center"><i class="fa fa-laptop fa-fw" aria-hidden="true"><span class="sr-only">{{ trans('admin/custom_fields/general.show_in_requestable_list_short') }}</span></i></th> <th data-sortable="true" data-searchable="true" class="text-center"><i class="fa fa-laptop fa-fw" aria-hidden="true"><span class="sr-only">{{ trans('admin/custom_fields/general.show_in_requestable_list_short') }}</span></i></th>
<th data-sortable="true" data-searchable="true" class="text-center"><i class="fa-solid fa-fingerprint"><span class="sr-only">{{ trans('admin/custom_fields/general.unique') }}</span></i></th>
<th data-sortable="true" data-searchable="true" class="text-center">{{ trans('admin/custom_fields/general.field_element_short') }}</th> <th data-sortable="true" data-searchable="true" class="text-center">{{ trans('admin/custom_fields/general.field_element_short') }}</th>
<th data-searchable="true">{{ trans('admin/custom_fields/general.fieldsets') }}</th> <th data-searchable="true">{{ trans('admin/custom_fields/general.fieldsets') }}</th>
<th>{{ trans('button.actions') }}</th> <th>{{ trans('button.actions') }}</th>
@ -176,6 +177,7 @@
<td class="text-center">{!! ($field->display_in_user_view=='1' ? '<i class="fa fa-check text-success"></i>' : '<i class="fa fa-times text-danger"></i>') !!}</td> <td class="text-center">{!! ($field->display_in_user_view=='1' ? '<i class="fa fa-check text-success"></i>' : '<i class="fa fa-times text-danger"></i>') !!}</td>
<td class="text-center">{!! ($field->show_in_email=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td> <td class="text-center">{!! ($field->show_in_email=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td class="text-center">{!! ($field->show_in_requestable_list=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td> <td class="text-center">{!! ($field->show_in_requestable_list=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td class="text-center">{!! ($field->is_unique=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td>{{ $field->element }}</td> <td>{{ $field->element }}</td>
<td> <td>
@foreach($field->fieldset as $fieldset) @foreach($field->fieldset as $fieldset)

View file

@ -62,7 +62,7 @@
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('null_expected_checkin_date', '1', false, ['checked' => 'false']) }} {{ Form::checkbox('null_expected_checkin_date', '1', false) }}
{{ trans_choice('general.set_to_null', count($assets), ['asset_count' => count($assets)]) }} {{ trans_choice('general.set_to_null', count($assets), ['asset_count' => count($assets)]) }}
</label> </label>
</div> </div>

View file

@ -37,21 +37,20 @@
@elseif ($field->element=='checkbox') @elseif ($field->element=='checkbox')
<!-- Checkboxes --> <!-- Checkboxes -->
@foreach ($field->formatFieldValuesAsArray() as $key => $value) @foreach ($field->formatFieldValuesAsArray() as $key => $value)
<div> <label class="form-control">
<label> <input type="checkbox" value="{{ $value }}" name="{{ $field->db_column_name() }}[]" {{ isset($item) ? (in_array($value, array_map('trim', explode(',', $item->{$field->db_column_name()}))) ? ' checked="checked"' : '') : (Request::old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($key, array_map('trim', explode(',', $field->defaultValue($model->id)))) ? ' checked="checked"' : '')) }}>
<input type="checkbox" value="{{ $value }}" name="{{ $field->db_column_name() }}[]" class="minimal" {{ isset($item) ? (in_array($value, array_map('trim', explode(',', $item->{$field->db_column_name()}))) ? ' checked="checked"' : '') : (Request::old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($key, array_map('trim', explode(',', $field->defaultValue($model->id)))) ? ' checked="checked"' : '')) }}> {{ $value }}
{{ $value }} </label>
</label>
</div>
@endforeach @endforeach
@elseif ($field->element=='radio') @elseif ($field->element=='radio')
@foreach ($field->formatFieldValuesAsArray() as $value) @foreach ($field->formatFieldValuesAsArray() as $value)
<div>
<label> <label class="form-control">
<input type="radio" value="{{ $value }}" name="{{ $field->db_column_name() }}" class="minimal" {{ isset($item) ? ($item->{$field->db_column_name()} == $value ? ' checked="checked"' : '') : (Request::old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}> <input type="radio" value="{{ $value }}" name="{{ $field->db_column_name() }}" {{ isset($item) ? ($item->{$field->db_column_name()} == $value ? ' checked="checked"' : '') : (Request::old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}>
{{ $value }} {{ $value }}
</label> </label>
</div>
@endforeach @endforeach
@endif @endif

View file

@ -115,17 +115,19 @@
@endif @endif
@if ($messages = Session::get('bulk_errors')) @if ($messages = Session::get('bulk_asset_errors'))
<div class="col-md-12"> <div class="col-md-12">
<div class="alert alert alert-danger fade in"> <div class="alert alert alert-danger fade in">
<button type="button" class="close" data-dismiss="alert">&times;</button> <button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fas fa-exclamation-triangle faa-pulse animated"></i> <i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<strong>{{ trans('general.notification_error') }}: </strong> <strong>{{ trans('general.notification_error') }}: </strong>
{{ trans('general.notification_bulk_error_hint') }} {{ trans('general.notification_bulk_error_hint') }}
@foreach($messages as $message) @foreach($messages as $key => $message)
@for ($x = 0; $x < count($message); $x++)
<ul> <ul>
<li>{{ $message }}</li> <li>{{ $message[$x] }}</li>
</ul> </ul>
@endfor
@endforeach @endforeach
</div> </div>
</div> </div>

View file

@ -530,15 +530,37 @@
function changeLogFormatter(value) { function changeLogFormatter(value) {
var result = ''; var result = '';
var pretty_index = '';
for (var index in value) { for (var index in value) {
result += index + ': <del>' + value[index].old + '</del> <i class="fas fa-long-arrow-alt-right" aria-hidden="true"></i> ' + value[index].new + '<br>'
// Check if it's a custom field
if (index.startsWith('_snipeit_')) {
pretty_index = index.replace("_snipeit_", "Custom:_");
} else {
pretty_index = index;
}
extra_pretty_index = prettyLog(pretty_index);
result += extra_pretty_index + ': <del>' + value[index].old + '</del> <i class="fas fa-long-arrow-alt-right" aria-hidden="true"></i> ' + value[index].new + '<br>'
} }
return result; return result;
} }
function prettyLog(str) {
let frags = str.split('_');
for (let i = 0; i < frags.length; i++) {
frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
}
return frags.join(' ');
}
// Create a linked phone number in the table list // Create a linked phone number in the table list
function phoneFormatter(value) { function phoneFormatter(value) {