Merge branch 'develop' into feature/sc-24120

This commit is contained in:
Brady Wetherington 2023-12-14 13:28:17 +00:00 committed by GitHub
commit 430f1b0212
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 887 additions and 313 deletions

View file

@ -1,4 +1,4 @@
FROM alpine:3.17.3
FROM alpine:3.18.5
# Apache + PHP
RUN apk add --no-cache \
apache2 \

View file

@ -1,5 +1,7 @@
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
[![All Contributors](https://img.shields.io/badge/all_contributors-330-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
![snipe-it-by-grok](https://github.com/snipe/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade) [![Tests](https://github.com/snipe/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-330-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System

View file

@ -84,35 +84,36 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/assetmodels',
'storage/private_uploads/users',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
];
$private_files = [
'storage/oauth-private.key',
'storage/oauth-public.key',
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
'public/uploads/categories',
'public/uploads/manufacturers',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/consumables',
'public/uploads/departments',
'public/uploads/avatars',
'public/uploads/suppliers',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/locations',
'public/uploads/accessories',
'public/uploads/models',
'public/uploads/categories',
'public/uploads/avatars',
'public/uploads/manufacturers',
'public/uploads/models',
'public/uploads/suppliers',
];
$public_files = [

View file

@ -354,7 +354,7 @@ class Helper
if ($index >= $total_colors) {
\Log::error('Status label count is '.$index.' and exceeds the allowed count of 266.');
\Log::info('Status label count is '.$index.' and exceeds the allowed count of 266.');
//patch fix for array key overflow (color count starts at 1, array starts at 0)
$index = $index - $total_colors - 1;

View file

@ -116,41 +116,17 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
// create a new model instance
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(e($request->input('asset_id')));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot add a maintenance for that asset'));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->user_id = Auth::id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
$maintenance = new AssetMaintenance();
$maintenance->fill($request->all());
$maintenance->user_id = Auth::id();
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.create.success')));
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
@ -158,67 +134,41 @@ class AssetMaintenancesController extends Controller
* Validates and stores an update to an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $assetMaintenanceId
* @param int $id
* @param int $request
* @version v1.0
* @since [v4.0]
* @return string JSON
*/
public function update(Request $request, $assetMaintenanceId = null)
public function update(Request $request, $id)
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
// Can this user manage this asset?
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
}
$assetMaintenance->supplier_id = e($request->input('supplier_id'));
$assetMaintenance->is_warranty = e($request->input('is_warranty'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
// The asset this miantenance is attached to is not valid or has been deleted
if (!$maintenance->asset) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$maintenance->fill($request->all());
if (($assetMaintenance->completion_date == null)
) {
if (($assetMaintenance->asset_maintenance_time !== 0)
|| (! is_null($assetMaintenance->asset_maintenance_time))
) {
$assetMaintenance->asset_maintenance_time = null;
}
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
}
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.edit.success')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
}
/**
* Delete an asset maintenance
*

View file

@ -885,13 +885,17 @@ class AssetsController extends Controller
public function checkin(Request $request, $asset_id)
{
$this->authorize('checkin', Asset::class);
$asset = Asset::findOrFail($asset_id);
$asset = Asset::with('model')->findOrFail($asset_id);
$this->authorize('checkin', $asset);
$target = $asset->assignedTo;
if (is_null($target)) {
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.already_checked_in')));
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag'=> e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.already_checked_in')));
}
$asset->expected_checkin = null;
@ -925,7 +929,11 @@ class AssetsController extends Controller
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.success')));
return response()->json(Helper::formatStandardApiResponse('success', [
'asset_tag'=> e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error')));

View file

@ -353,6 +353,7 @@ class UsersController extends Controller
$user = new User;
$user->fill($request->all());
$user->created_by = Auth::user()->id;
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');

View file

@ -88,7 +88,7 @@ class AssetModelsController extends Controller
$model->requestable = Request::has('requestable');
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = e($request->input('fieldset_id'));
$model->fieldset_id = $request->input('fieldset_id');
}
$model = $request->handleImages($model);
@ -101,7 +101,6 @@ class AssetModelsController extends Controller
}
}
// Redirect to the new model page
return redirect()->route('models.index')->with('success', trans('admin/models/message.create.success'));
}
@ -166,9 +165,6 @@ class AssetModelsController extends Controller
$this->removeCustomFieldsDefaultValues($model);
if ($request->input('fieldset_id') == '') {
$model->fieldset_id = null;
} else {
$model->fieldset_id = $request->input('fieldset_id');
if ($this->shouldAddDefaultValues($request->input())) {
@ -176,7 +172,7 @@ class AssetModelsController extends Controller
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
}
}

View file

@ -98,7 +98,7 @@ class AssetCheckoutController extends Controller
}
}
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $request->get('name'))) {
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.checkout.success'));
}

View file

@ -7,6 +7,8 @@ use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Statuslabel;
use App\Models\Setting;
use App\View\Label;
use Illuminate\Http\Request;
@ -23,6 +25,13 @@ class BulkAssetsController extends Controller
/**
* Display the bulk edit page.
*
* This method is super weird because it's kinda of like a controller within a controller.
* It's main function is to determine what the bulk action in, and then return a view with
* the information that view needs, be it bulk delete, bulk edit, restore, etc.
*
* This is something that made sense at the time, but sort of doesn't make sense now. A JS front-end to determine form
* action would make a lot more sense here and make things a lot more clear.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param int $assetId
@ -33,6 +42,9 @@ class BulkAssetsController extends Controller
{
$this->authorize('view', Asset::class);
/**
* No asset IDs were passed
*/
if (! $request->filled('ids')) {
return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
@ -41,40 +53,33 @@ class BulkAssetsController extends Controller
$bulk_back_url = request()->headers->get('referer');
session(['bulk_back_url' => $bulk_back_url]);
$asset_ids = array_values(array_unique($request->input('ids')));
//custom fields logic
$asset_custom_field = Asset::with(['model.fieldset.fields', 'model'])->whereIn('id', $asset_ids)->whereHas('model', function ($query) {
return $query->where('fieldset_id', '!=', null);
})->get();
$asset_ids = $request->input('ids');
$assets = Asset::with('assignedTo', 'location', 'model')->find($asset_ids);
$models = $asset_custom_field->unique('model_id');
$models = $assets->unique('model_id');
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
if ($request->filled('bulk_actions')) {
switch ($request->input('bulk_actions')) {
case 'labels':
$this->authorize('view', Asset::class);
$assets_found = Asset::find($asset_ids);
if ($assets_found->isEmpty()){
return redirect()->back();
}
return (new Label)
->with('assets', $assets_found)
->with('assets', $assets)
->with('settings', Setting::getSettings())
->with('bulkedit', true)
->with('count', 0);
case 'delete':
$this->authorize('delete', Asset::class);
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
$assets->each(function ($assets) {
$this->authorize('delete', $assets);
});
return view('hardware/bulk-delete')->with('assets', $assets);
@ -85,11 +90,11 @@ class BulkAssetsController extends Controller
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
});
return view('hardware/bulk-restore')->with('assets', $assets);
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@ -117,25 +122,31 @@ class BulkAssetsController extends Controller
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
if (Session::exists('ids')) {
$assets = Session::get('ids');
} elseif (! $request->filled('ids') || count($request->input('ids')) <= 0) {
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$assets = array_keys($request->input('ids'));
if ($request->anyFilled($custom_field_columns)) {
$custom_fields_present = true;
} else {
$custom_fields_present = false;
}
$assets = Asset::whereIn('id', array_keys($request->input('ids')))->get();
/**
* If ANY of these are filled, prepare to update the values on the assets.
*
* Additional checks will be needed for some of them to make sure the values
* make sense (for example, changing the status ID to something incompatible with
* its checkout status.
*/
if (($request->filled('purchase_date'))
|| ($request->filled('expected_checkin'))
|| ($request->filled('purchase_cost'))
@ -154,15 +165,22 @@ class BulkAssetsController extends Controller
|| ($request->anyFilled($custom_field_columns))
) {
foreach ($assets as $assetId) {
// Let's loop through those assets and build an update array
foreach ($assets as $asset) {
$this->update_array = [];
/**
* Leave out model_id and status here because we do math on that later. We have to do some extra
* validation and checks on those two.
*
* It's tempting to make these match the request check above, but some of these values require
* extra work to make sure the data makes sense.
*/
$this->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
->conditionallyAddItem('status_id')
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date');
@ -170,6 +188,9 @@ class BulkAssetsController extends Controller
$this->conditionallyAddItem($custom_field_column);
}
/**
* Blank out fields that were requested to be blanked out via checkbox
*/
if ($request->input('null_purchase_date')=='1') {
$this->update_array['purchase_date'] = null;
}
@ -186,7 +207,6 @@ class BulkAssetsController extends Controller
$this->update_array['purchase_cost'] = $request->input('purchase_cost');
}
if ($request->filled('company_id')) {
$this->update_array['company_id'] = $request->input('company_id');
if ($request->input('company_id') == 'clear') {
@ -194,48 +214,92 @@ class BulkAssetsController extends Controller
}
}
/**
* We're trying to change the model ID - we need to do some extra checks here to make sure
* the custom field values work for the custom fieldset rules around this asset. Uniqueness
* and requiredness across the fieldset is particularly important, since those are
* fieldset-specific attributes.
*/
if ($request->filled('model_id')) {
$this->update_array['model_id'] = AssetModel::find($request->input('model_id'))->id;
}
/**
* We're trying to change the status ID - we need to do some extra checks here to
* make sure the status label type is one that makes sense for the state of the asset,
* for example, we shouldn't be able to make an asset archived if it's currently assigned
* to someone/something.
*/
if ($request->filled('status_id')) {
$updated_status = Statuslabel::find($request->input('status_id'));
// We cannot assign a non-deployable status type if the asset is already assigned.
// This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one.
if (
($asset->assigned_to == '')
|| ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1')
) {
$this->update_array['status_id'] = $updated_status->id;
}
}
/**
* We're changing the location ID - figure out which location we should apply
* this change to:
*
* 0 - RTD location only
* 1 - location ID and RTD location ID
* 2 - location ID only
*
* Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe
*/
if ($request->filled('rtd_location_id')) {
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) {
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
}
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
}
}
/**
* ------------------------------------------------------------------------------
* ANYTHING that happens past this foreach
* WILL NOT BE logged in the edit log_meta data
* ------------------------------------------------------------------------------
*/
$changed = [];
$asset = Asset::find($assetId);
foreach ($this->update_array as $key => $value) {
if ($this->update_array[$key] != $asset->{$key}) {
$changed[$key]['old'] = $asset->{$key};
$changed[$key]['new'] = $this->update_array[$key];
}
}
if ($custom_fields_present) {
/**
* Start all the custom fields shenanigans
*/
$model = $asset->model()->first();
// Does the model have a fieldset?
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// Use the rules of the new model fieldsets if the model changed
if ($request->filled('model_id')) {
$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) {
foreach ($assetCustomFields->fields as $field) {
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted=='1')) {
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
/*
@ -260,7 +324,6 @@ class BulkAssetsController extends Controller
*/
} 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
@ -273,9 +336,7 @@ class BulkAssetsController extends Controller
}
} // endforeach
} // end custom field check
} // end custom fields handler
}
// Check if it passes validation, and then try to save

View file

@ -266,7 +266,7 @@ class Asset extends Depreciable
/**
* Determines if an asset is available for checkout.
* This checks to see if the it's checked out to an invalid (deleted) user
* This checks to see if it's checked out to an invalid (deleted) user
* OR if the assigned_to and deleted_at fields on the asset are empty AND
* that the status is deployable
*
@ -753,7 +753,7 @@ class Asset extends Depreciable
}
/**
* Establishes the asset -> status relationship
* Establishes the asset -> license seats relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
@ -921,6 +921,27 @@ class Asset extends Depreciable
return $cost;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets the requestable to a boolean 0 or 1. This accounts for forms or API calls that
* explicitly pass the requestable field but it has a null or empty value.
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
* @param $value
* @return void
*/
public function setRequestableAttribute($value)
{
$this->attributes['requestable'] = (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES

View file

@ -20,10 +20,9 @@ class AssetMaintenance extends Model implements ICompanyableChild
use SoftDeletes;
use CompanyableChildTrait;
use ValidatingTrait;
protected $casts = [
'start_date' => 'datetime',
'completion_date' => 'datetime',
];
protected $table = 'asset_maintenances';
protected $rules = [
'asset_id' => 'required|integer',
@ -31,12 +30,31 @@ class AssetMaintenance extends Model implements ICompanyableChild
'asset_maintenance_type' => 'required',
'title' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date',
'completion_date' => 'nullable|date',
'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable',
'notes' => 'string|nullable',
'cost' => 'numeric|nullable',
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'asset_id',
'supplier_id',
'asset_maintenance_type',
'is_warranty',
'start_date',
'completion_date',
'asset_maintenance_time',
'notes',
'cost',
];
use Searchable;
/**

View file

@ -74,12 +74,16 @@
"unicodeveloper/laravel-password": "^1.0",
"watson/validating": "^6.1"
},
"suggest": {
"ext-ldap": "*"
},
"require-dev": {
"brianium/paratest": "^6.6",
"fakerphp/faker": "^1.16",
"mockery/mockery": "^1.4",
"nunomaduro/larastan": "^1.0",
"nunomaduro/phpinsights": "^2.7",
"php-mock/php-mock-phpunit": "^2.8",
"phpunit/php-token-stream": "^3.1",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5",

224
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "348f96db24a0f8dfb595ee38b38b34eb",
"content-hash": "f4f3b6b02d044ed3e54cdd509b01c3dc",
"packages": [
{
"name": "alek13/slack",
@ -7011,16 +7011,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.14",
"version": "3.0.34",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef"
"reference": "56c79f16a6ae17e42089c06a2144467acc35348a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2f0b7af658cbea265cbb4a791d6c29a6613f98ef",
"reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a",
"reference": "56c79f16a6ae17e42089c06a2144467acc35348a",
"shasum": ""
},
"require": {
@ -7032,6 +7032,7 @@
"phpunit/phpunit": "*"
},
"suggest": {
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
@ -7100,7 +7101,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.14"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.34"
},
"funding": [
{
@ -7116,7 +7117,7 @@
"type": "tidelift"
}
],
"time": "2022-04-04T05:15:45+00:00"
"time": "2023-11-27T11:13:31+00:00"
},
{
"name": "phpspec/prophecy",
@ -14100,6 +14101,213 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "php-mock/php-mock",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock.git",
"reference": "6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock/zipball/6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d",
"reference": "6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0 || ^8.0",
"phpunit/php-text-template": "^1 || ^2 || ^3"
},
"replace": {
"malkusch/php-mock": "*"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock."
},
"type": "library",
"autoload": {
"files": [
"autoload.php"
],
"psr-4": {
"phpmock\\": [
"classes/",
"tests/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.",
"homepage": "https://github.com/php-mock/php-mock",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"stub",
"test",
"test double",
"testing"
],
"support": {
"issues": "https://github.com/php-mock/php-mock/issues",
"source": "https://github.com/php-mock/php-mock/tree/2.4.1"
},
"funding": [
{
"url": "https://github.com/michalbundyra",
"type": "github"
}
],
"time": "2023-06-12T20:48:52+00:00"
},
{
"name": "php-mock/php-mock-integration",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock-integration.git",
"reference": "04f4a8d5442ca457b102b5204673f77323e3edb5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/04f4a8d5442ca457b102b5204673f77323e3edb5",
"reference": "04f4a8d5442ca457b102b5204673f77323e3edb5",
"shasum": ""
},
"require": {
"php": ">=5.6",
"php-mock/php-mock": "^2.4",
"phpunit/php-text-template": "^1 || ^2 || ^3"
},
"require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10"
},
"type": "library",
"autoload": {
"psr-4": {
"phpmock\\integration\\": "classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "Integration package for PHP-Mock",
"homepage": "https://github.com/php-mock/php-mock-integration",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"stub",
"test",
"test double"
],
"support": {
"issues": "https://github.com/php-mock/php-mock-integration/issues",
"source": "https://github.com/php-mock/php-mock-integration/tree/2.2.1"
},
"funding": [
{
"url": "https://github.com/michalbundyra",
"type": "github"
}
],
"time": "2023-02-13T09:51:29+00:00"
},
{
"name": "php-mock/php-mock-phpunit",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock-phpunit.git",
"reference": "56edee85ad3232caa0202f98f2a3c899ab16bdb7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/56edee85ad3232caa0202f98f2a3c899ab16bdb7",
"reference": "56edee85ad3232caa0202f98f2a3c899ab16bdb7",
"shasum": ""
},
"require": {
"php": ">=7",
"php-mock/php-mock-integration": "^2.2.1",
"phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17"
},
"require-dev": {
"mockery/mockery": "^1.3.6"
},
"type": "library",
"autoload": {
"files": [
"autoload.php"
],
"psr-4": {
"phpmock\\phpunit\\": "classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "Mock built-in PHP functions (e.g. time()) with PHPUnit. This package relies on PHP's namespace fallback policy. No further extension is needed.",
"homepage": "https://github.com/php-mock/php-mock-phpunit",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"phpunit",
"stub",
"test",
"test double",
"testing"
],
"support": {
"issues": "https://github.com/php-mock/php-mock-phpunit/issues",
"source": "https://github.com/php-mock/php-mock-phpunit/tree/2.8.0"
},
"funding": [
{
"url": "https://github.com/michalbundyra",
"type": "github"
}
],
"time": "2023-10-30T07:06:12+00:00"
},
{
"name": "php-parallel-lint/php-parallel-lint",
"version": "v1.3.2",
@ -16600,5 +16808,5 @@
"ext-pdo": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -99,10 +99,22 @@ class CustomFieldFactory extends Factory
return [
'name' => 'Test Checkbox',
'help_text' => 'This is a sample checkbox.',
'field_values' => "One\nTwo\nThree",
'field_values' => "One\r\nTwo\r\nThree",
'element' => 'checkbox',
];
});
}
public function testRadio()
{
return $this->state(function () {
return [
'name' => 'Test Radio',
'help_text' => 'This is a sample radio.',
'field_values' => "One\r\nTwo\r\nThree",
'element' => 'radio',
];
});
}
}

View file

@ -35,6 +35,7 @@ class CustomFieldSeeder extends Seeder
CustomField::factory()->count(1)->macAddress()->create();
CustomField::factory()->count(1)->testEncrypted()->create();
CustomField::factory()->count(1)->testCheckbox()->create();
CustomField::factory()->count(1)->testRadio()->create();
DB::table('custom_field_custom_fieldset')->insert([
@ -96,6 +97,19 @@ class CustomFieldSeeder extends Seeder
'required' => 0,
],
[
'custom_field_id' => '8',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '8',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 0,
],
]);
}
}

View file

@ -29,7 +29,7 @@
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.13.2",
"alpinejs": "^3.13.3",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,8 @@
{
"/js/build/app.js": "/js/build/app.js?id=41293fc7aa00ece89fd524e1e0e31a68",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374",
"/css/build/overrides.css": "/css/build/overrides.css?id=1681749b20329c40a052156ea4cb42fe",
"/css/build/app.css": "/css/build/app.css?id=cc0fd2d77504fdd7f03e91e2369d02a9",
"/css/build/overrides.css": "/css/build/overrides.css?id=8453824ff928a55d685947c84e6b3079",
"/css/build/app.css": "/css/build/app.css?id=ddf74e5777fbad72decf760fe8e57570",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=dc383f8560a8d4adb51d44fb4043e03b",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=6f0563e726c2fe4fab4026daaa5bfdf2",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=e6e53eef152bba01a4c666a4d8b01117",
@ -12,13 +12,13 @@
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=7d92dea45d94be7e1d4e427c728d335d",
"/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=6fe68325d5356197672c27bc77cedcb4",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=8ca888bbc050d9680cbb65021382acba",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=bdfc704731682c67645a2248b0b8d2d7",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=b061bb141af3bdb6280c6ee772cf8f4f",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=d419cb63a12dc175d71645c876bfc2ab",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/all.css": "/css/dist/all.css?id=391b67f142742a6b11a92b042c6d475c",
"/css/dist/all.css": "/css/dist/all.css?id=5fa237d9baa78a00f78c75819cd794e6",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=a656b2d865fe379d8851757e8e4001ef",
@ -37,7 +37,7 @@
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=d419cb63a12dc175d71645c876bfc2ab",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=bdfc704731682c67645a2248b0b8d2d7",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=b061bb141af3bdb6280c6ee772cf8f4f",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=f677207c6cf9678eb539abecb408c374",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=07273f6ca3c698a39e8fc2075af4fa07",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",

View file

@ -399,7 +399,7 @@ img.navbar-brand-img, .navbar-brand>img {
}
.icon-med {
font-size: 20px;
font-size: 14px;
color: #889195;
}

View file

@ -124,11 +124,11 @@ a {
--button-primary: darken(@black, 25%);
--button-hover: darken(@black, 30%);
--header: @black; /* Use same as Header picker */
--text-main: #BBB;
--text-main: #fff;
--text-sub: #9b9b9b;
--link: #AAA; /* Use same as Header picker, lighten by 70% */
--visited-link: lighten(@black, 40%); /* Use same as Header picker, lighten by 70% */
--hover-link: lighten(@black, 45%); /* Use same as Header picker, lighten by 70% */
--link: #fff; /* Use same as Header picker, lighten by 70% */
--visited-link: #fff; /* Use same as Header picker, lighten by 70% */
--hover-link: #949494; /* Use same as Header picker, lighten by 70% */
--nav-link: #FFF; /* Use same as Header picker */
--light-link: #fff; /* Use same as Header picker */
}
@ -193,18 +193,6 @@ h2.task_menu{
color: var(--text-main);
}
a:link {
color: var(--link);
}
a:visited {
color: var(--nav-link);
}
a:hover {
color: var(--hover-link);
}
.btn-primary.hover {
color: var(--nav-link);
}
@ -318,8 +306,11 @@ input[type=text], input[type=search] {
background-color: var(--back-sub);
color: var(--text-main);
}
.search-highlight, .search-highlight:hover{
background-color: var(--back-sub) !important;
}
.input-group, .input-group-addon {
background-color: var(--back-sub)!important;
background-color: var(--back-sub);
color: var(--text-main);
}
#licensesTable>tbody>tr>td>nobr>a>i.fa {
@ -363,11 +354,11 @@ input[type=text], input[type=search] {
}
.select2-container--default .select2-results__option[aria-selected=true], .select2-container--default .select2-results__option[aria-selected=true]:hover {
background-color: var(--back-sub);
color: var(--header);
color: var(--nav-link);
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--header);
color: var(--back-main);
background-color: var(--back-sub);
color: var(--visited-link);
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: var(--text-main);
@ -426,9 +417,6 @@ a {
color: var(--hover-link);
text-decoration: underline;
}
&:visited {
color: var(--visited-link)
}
}
.row-striped {
@ -464,3 +452,6 @@ a {
div.container.row-new-striped{
background-color: var(--back-sub);
}
.table > thead > tr > td.danger, .table > tbody > tr > td.danger, .table > tfoot > tr > td.danger, .table > thead > tr > th.danger, .table > tbody > tr > th.danger, .table > tfoot > tr > th.danger, .table > thead > tr.danger > td, .table > tbody > tr.danger > td, .table > tfoot > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr.danger > th, .table > tfoot > tr.danger > th {
background-color: var(--back-sub);
}

View file

@ -9,6 +9,7 @@ return [
'ad_append_domain_help' => 'User isn\'t required to write "username@domain.local", they can just type "username".',
'admin_cc_email' => 'CC Email',
'admin_cc_email_help' => 'If you would like to send a copy of checkin/checkout emails that are sent to users to an additional email account, enter it here. Otherwise leave this field blank.',
'admin_settings' => 'Admin Settings',
'is_ad' => 'This is an Active Directory server',
'alerts' => 'Alerts',
'alert_title' => 'Update Notification Settings',

View file

@ -21,6 +21,7 @@ return array(
'manager' => 'Manager',
'managed_locations' => 'Managed Locations',
'name' => 'Name',
'nogroup' => 'No groups have been created yet. To add one, visit: ',
'notes' => 'Notes',
'password_confirm' => 'Confirm Password',
'password' => 'Password',

View file

@ -156,6 +156,7 @@ return [
'image_filetypes_help' => 'Accepted filetypes are jpg, webp, png, gif, and svg. Max upload size allowed is :size.',
'unaccepted_image_type' => 'This image file was not readable. Accepted filetypes are jpg, webp, png, gif, and svg. The mimetype of this file is: :mimetype.',
'import' => 'Import',
'import_this_file' => 'Map fields and process this file',
'importing' => 'Importing',
'importing_help' => 'You can import assets, accessories, licenses, components, consumables, and users via CSV file. <br><br>The CSV should be comma-delimited and formatted with headers that match the ones in the <a href="https://snipe-it.readme.io/docs/importing" target="_new">sample CSVs in the documentation</a>.',
'import-history' => 'Import History',
@ -491,5 +492,11 @@ return [
'upload_error' => 'Error uploading file. Please check that there are no empty rows and that no column names are duplicated.',
'copy_to_clipboard' => 'Copy to Clipboard',
'copied' => 'Copied!',
'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.',
'rtd_location_help' => 'This is the location of the asset when it is not checked out',
'item_not_found' => ':item_type ID :id does not exist or has been deleted',
'action_permission_denied' => 'You do not have permission to :action :item_type ID :id',
'action_permission_generic' => 'You do not have permission to :action this :item_type',
'edit' => 'edit',
];

View file

@ -491,7 +491,7 @@
<thead>
<tr>
<th class="col-md-4">{{ trans('general.name') }}</th>
<th class="col-md-4">{{ trans('admin/hardware/form.serial') }}</th>
<th class="col-md-4">{{ trans('admin/licenses/form.license_key') }}</th>
<th class="col-md-4">{{ trans('general.category') }}</th>
</tr>
</thead>

View file

@ -30,7 +30,7 @@
<tr>
<td></td>
<td>{{ trans('admin/hardware/table.id') }}</td>
<td>{{ trans('admin/hardware/table.name') }}</td>
<td>{{ trans('admin/hardware/form.name') }}</td>
<td>{{ trans('admin/hardware/table.location')}}</td>
</tr>
</thead>

View file

@ -19,18 +19,22 @@
<p>{{ trans('admin/hardware/form.bulk_update_help') }}</p>
<div class="callout callout-warning">
<i class="fas fa-exclamation-triangle"></i> {{ trans_choice('admin/hardware/form.bulk_update_warn', count($assets), ['asset_count' => count($assets)]) }}
@if (count($models) > 0)
{{ trans_choice('admin/hardware/form.bulk_update_with_custom_field', count($models), ['asset_model_count' => count($models)]) }}
@endif
</div>
<form class="form-horizontal" method="post" action="{{ route('hardware/bulksave') }}" autocomplete="off" role="form">
{{ csrf_field() }}
<div class="box box-default">
<div class="box-body">
<div class="callout callout-warning">
<i class="fas fa-exclamation-triangle"></i> {{ trans_choice('admin/hardware/form.bulk_update_warn', count($assets), ['asset_count' => count($assets)]) }}
@if (count($models) > 0)
{{ trans_choice('admin/hardware/form.bulk_update_with_custom_field', count($models), ['asset_model_count' => count($models)]) }}
@endif
</div>
<!-- Purchase Date -->
<div class="form-group {{ $errors->has('purchase_date') ? ' has-error' : '' }}">
<label for="purchase_date" class="col-md-3 control-label">{{ trans('admin/hardware/form.date') }}</label>
@ -76,6 +80,7 @@
</label>
<div class="col-md-7">
{{ Form::select('status_id', $statuslabel_list , old('status_id'), array('class'=>'select2', 'style'=>'width:100%', 'aria-label'=>'status_id')) }}
<p class="help-block">{{ trans('general.status_compatibility') }}</p>
{!! $errors->first('status_id', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>

View file

@ -65,7 +65,7 @@
@endif
@include ('partials.forms.edit.notes')
@include ('partials.forms.edit.location-select', ['translated_name' => trans('admin/hardware/form.default_location'), 'fieldname' => 'rtd_location_id'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('admin/hardware/form.default_location'), 'fieldname' => 'rtd_location_id', 'help_text' => trans('general.rtd_location_help')])
@include ('partials.forms.edit.requestable', ['requestable_text' => trans('admin/hardware/general.requestable')])

View file

@ -81,6 +81,8 @@
<thead>
<tr>
<th>{{ trans('general.asset_tag') }}</th>
<th>{{ trans('general.asset_model') }}</th>
<th>{{ trans('general.model_no') }}</th>
<th>{{ trans('general.quickscan_checkin_status') }}</th>
<th></th>
</tr>
@ -126,7 +128,7 @@
data : formData,
success : function (data) {
if (data.status == 'success') {
$('#checkedin tbody').prepend("<tr class='success'><td>" + data.payload.asset + "</td><td>" + data.messages + "</td><td><i class='fas fa-check text-success'></i></td></tr>");
$('#checkedin tbody').prepend("<tr class='success'><td>" + data.payload.asset_tag + "</td><td>" + data.payload.model + "</td><td>" + data.payload.model_number + "</td><td>" + data.messages + "</td><td><i class='fas fa-check text-success'></i></td></tr>");
incrementOnSuccess();
} else {
handlecheckinFail(data);
@ -146,17 +148,21 @@
});
function handlecheckinFail (data) {
if (data.payload.asset) {
var asset = data.payload.asset;
if (data.payload.asset_tag) {
var asset_tag = data.payload.asset_tag;
var model = data.payload.model;
var model_number = data.payload.model_number;
} else {
var asset = '';
var asset_tag = '';
var model = '';
var model_number = '';
}
if (data.messages) {
var messages = data.messages;
} else {
var messages = '';
}
$('#checkedin tbody').prepend("<tr class='danger'><td>" + asset + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
$('#checkedin tbody').prepend("<tr class='danger'><td>" + asset_tag + "</td><td>" + model + "</td><td>" + model_number + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
}
function incrementOnSuccess() {

View file

@ -46,7 +46,8 @@
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-3" data-sortable="true">{{ trans('admin/hardware/table.requesting_user') }}</th>
<th class="col-md-2">{{ trans('admin/hardware/table.requested_date') }}</th>
<th class="col-md-1">{{ trans('button.actions') }}</th> <th></th>
<th class="col-md-1">{{ trans('button.actions') }}</th>
<th class="col-md-1">{{ trans('general.checkout') }}</th>
</tr>
</thead>
<tbody>

View file

@ -1206,15 +1206,15 @@
<thead>
<tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th class="col-sm-2" data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th class="col-sm-1" data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th class="col-sm-1" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th class="col-sm-2" data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
<th class="col-sm-2" data-field="note">{{ trans('general.notes') }}</th>
<th class="col-md-3" data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
<th class="col-md-3" data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
<th class="col-sm-2" data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
<th data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
<th data-field="note">{{ trans('general.notes') }}</th>
<th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
<th data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
<th data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
</tr>
</thead>
</table>

View file

@ -624,7 +624,7 @@
@can('import')
<li{!! (Request::is('import/*') ? ' class="active"' : '') !!}>
<a href="{{ route('imports.index') }}">
<i class="fas fa-cloud-download-alt fa-fw" aria-hidden="true"></i>
<i class="fas fa-cloud-upload-alt fa-fw" aria-hidden="true"></i>
<span>{{ trans('general.import') }}</span>
</a>
</li>

View file

@ -36,7 +36,7 @@
<!-- Serial -->
<div class="form-group">
<label class="col-sm-2 control-label">{{ trans('admin/hardware/form.serial') }}</label>
<label class="col-sm-2 control-label">{{ trans('admin/licenses/form.license_key') }}</label>
<div class="col-md-6">
<p class="form-control-static">
@can('viewKeys', $licenseSeat->license)

View file

@ -43,7 +43,7 @@
<!-- Serial -->
<div class="form-group">
<label class="col-sm-3 control-label">{{ trans('admin/hardware/form.serial') }}</label>
<label class="col-sm-3 control-label">{{ trans('admin/licenses/form.license_key') }}</label>
<div class="col-md-9">
<p class="form-control-static" style="word-wrap: break-word;">
@can('viewKeys', $license)

View file

@ -1,6 +1,9 @@
<span> {{-- This <span> doesn't seem to fix it, neither does a div? --}}
<span>
<div class="form-group{{ $errors->has('custom_fieldset') ? ' has-error' : '' }}">
<label for="custom_fieldset" class="col-md-3 control-label">{{ trans('admin/models/general.fieldset') }}</label>
<label for="custom_fieldset" class="col-md-3 control-label">
{{ trans('admin/models/general.fieldset') }}
</label>
<div class="col-md-5">
{{ Form::select('fieldset_id', Helper::customFieldsetList(), old('fieldset_id', $fieldset_id), array('class'=>'select2 js-fieldset-field livewire-select2', 'style'=>'width:100%; min-width:350px', 'aria-label'=>'custom_fieldset', 'data-livewire-component' => $_instance->id)) }}
{!! $errors->first('custom_fieldset', '<span class="alert-msg" aria-hidden="true"><br><i class="fas fa-times"></i> :message</span>') !!}
@ -12,43 +15,66 @@
</label>
</div>
</div>
@if ($this->add_default_values ) {{-- 'if the checkbox is enabled *AND* there are more than 0 fields in the fieldsset' --}}
<div style="padding-left: 10px; padding-bottom: 0px; margin-bottom: -15px;">
<div class="form-group">
@if ($fields)
@foreach ($fields as $field)
<div class="form-group">
<label class="col-md-3 control-label{{ $errors->has($field->name) ? ' has-error' : '' }}" for="default-value{{ $field->id }}">{{ $field->name }}</label>
<label class="col-md-3 control-label{{ $errors->has($field->name) ? ' has-error' : '' }}">{{ $field->name }}</label>
<div class="col-md-7">
@if ($field->format == "DATE")
<div class="input-group col-md-4" style="padding-left: 0px;">
<div class="input-group date" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-autoclose="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="default_values[{{ $field->id }}]" id="default-value{{ $field->id }}" value="{{ $field->defaultValue($model_id) }}">
<span class="input-group-addon"><i class="fas fa-calendar" aria-hidden="true"></i></span>
</div>
</div>
@elseif ($field->element == "text")
<input class="form-control m-b-xs" type="text" value="{{ $field->defaultValue($model_id) }}" id="default-value{{ $field->id }}" name="default_values[{{ $field->id }}]">
<input class="form-control" type="text" value="{{ $field->defaultValue($model_id) }}" id="default-value{{ $field->id }}" name="default_values[{{ $field->id }}]">
@elseif($field->element == "textarea")
<textarea class="form-control" style="width: 100%;" id="default-value{{ $field->id }}" name="default_values[{{ $field->id }}]">{{ $field->defaultValue($model_id) }}</textarea><br>
<textarea class="form-control" style="width: 100%;" id="default-value{{ $field->id }}" name="default_values[{{ $field->id }}]">{{ $field->defaultValue($model_id) }}</textarea>
@elseif($field->element == "listbox")
<select class="form-control m-b-xs" name="default_values[{{ $field->id }}]">
<select class="form-control" name="default_values[{{ $field->id }}]">
<option value=""></option>
@foreach(explode("\r\n", $field->field_values) as $field_value)
<option value="{{$field_value}}" {{ $field->defaultValue($model_id) == $field_value ? 'selected="selected"': '' }}>{{ $field_value }}</option>
@endforeach
</select>
@elseif($field->element == "radio")
@foreach(explode("\r\n", $field->field_values) as $field_value)
<input type='radio' name="default_values[{{ $field->id }}]" value="{{$field_value}}" {{ $field->defaultValue($model_id) == $field_value ? 'checked="checked"': '' }} />{{ $field_value }}<br />
<label class="col-md-3 form-control" for="{{ str_slug($field_value) }}">
<input id="{{ str_slug($field_value) }}" aria-label="{{ str_slug($field->name) }}" type='radio' name="default_values[{{ $field->id }}]" value="{{$field_value}}" {{ $field->defaultValue($model_id) == $field_value ? 'checked="checked"': '' }} />{{ $field_value }}
</label>
@endforeach
@elseif($field->element == "checkbox")
@foreach(explode("\r\n", $field->field_values) as $field_value)
<input type='checkbox' name="default_values[{{ $field->id }}][]" value="{{$field_value}}" {{ in_array($field_value, explode(', ',$field->defaultValue($model_id))) ? 'checked="checked"': '' }} /> {{ $field_value }}<br />
<label class="col-md-3 form-control" for="{{ str_slug($field_value) }}">
<input id="{{ str_slug($field_value) }}" type="checkbox" aria-label="{{ str_slug($field->name) }}" name="default_values[{{ $field->id }}][]" value="{{ $field_value }}"{{ in_array($field_value, explode(', ',$field->defaultValue($model_id))) ? ' checked="checked"': '' }}> {{ $field_value }}
</label>
@endforeach
@else
<span class="help-block form-error">
Unknown field element: {{ $field->element }}
@ -58,9 +84,8 @@
</div>
@endforeach
</div>
@endif
</div>
</div>
@endif
</span>

View file

@ -129,8 +129,8 @@
<td class="col-md-3">{{ Helper::getFormattedDateObject($currentFile->created_at, 'datetime', false) }}</td>
<td class="col-md-1">{{ Helper::formatFilesizeUnits($currentFile->filesize) }}</td>
<td class="col-md-1 text-right" style="white-space: nowrap;">
<button class="btn btn-sm btn-info" wire:click="selectFile({{ $currentFile->id }})">
<i class="fas fa-retweet fa-fw" aria-hidden="true"></i>
<button class="btn btn-sm btn-info" wire:click="selectFile({{ $currentFile->id }})" data-tooltip="true" title="{{ trans('general.import_this_file') }}">
<i class="fa-solid fa-list-check" aria-hidden="true"></i>
<span class="sr-only">{{ trans('general.import') }}</span>
</button>
<button class="btn btn-sm btn-danger" wire:click="destroy({{ $currentFile->id }})">

View file

@ -40,7 +40,8 @@
data-id-table="system-backups"
data-search="true"
data-side-pagination="client"
data-sort-order="asc"
data-sort-order="desc"
data-sort-name="modified_display"
id="system-backups"
class="table table-striped snipe-table">
<thead>

View file

@ -553,7 +553,7 @@
</div>
@endif
@else
<p>No groups have been created yet. Visit <code>Admin Settings > Permission Groups</code> to add one.</p>
<p>{{ trans('admin/users/table.nogroup') }} <code>{{ trans('admin/settings/general.admin_settings') }} <i class="fa fa-cogs"></i> > {{ trans('general.groups') }} <i class="fas fa-user-friends"></i></code> </p>
@endif
</div>

View file

@ -638,13 +638,14 @@
</div>
@endif
@if($user->getUserTotalCost()->total_user_cost > 0)
<div class="row">
<div class="col-md-3">
{{ trans('admin/users/table.total_assets_cost') }}
</div>
<div class="col-md-9">
{{Helper::formatCurrencyOutput($user->getUserTotalCost()->total_user_cost)}}
<a id="optional_info" class="text-primary">
<i class="fa fa-caret-right fa-2x" id="optional_info_icon"></i>
<strong>{{ trans('admin/hardware/form.optional_infos') }}</strong>
@ -658,13 +659,10 @@
{{trans('general.accessories').': '.Helper::formatCurrencyOutput($user->getUserTotalCost()->accessory_cost)}}<br>
</div>
</div>
</div>
</div><!--/.row-->
@endif
</div> <!--/end striped container-->
</div> <!-- end col-md-9 -->
</div> <!--/.row-->
</div><!-- /.tab-pane -->
@ -729,7 +727,7 @@
<thead>
<tr>
<th class="col-md-5">{{ trans('general.name') }}</th>
<th>{{ trans('admin/hardware/form.serial') }}</th>
<th>{{ trans('admin/licenses/form.license_key') }}</th>
<th data-footer-formatter="sumFormatter" data-fieldname="purchase_cost">{{ trans('general.purchase_cost') }}</th>
<th>{{ trans('admin/licenses/form.purchase_order') }}</th>
<th>{{ trans('general.order_number') }}</th>

View file

@ -3,6 +3,7 @@
namespace Tests\Support;
use App\Models\Setting;
use Illuminate\Support\Facades\Crypt;
class Settings
{
@ -67,6 +68,39 @@ class Settings
}
public function enableLdap(): Settings
{
return $this->update([
'ldap_enabled' => 1,
'ldap_server' => 'ldaps://ldap.example.com',
'ldap_uname' => 'fake_username',
'ldap_pword' => Crypt::encrypt("fake_password"),
'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com'
]);
}
public function enableAnonymousLdap(): Settings
{
return $this->update([
'ldap_enabled' => 1,
'ldap_server' => 'ldaps://ldap.example.com',
// 'ldap_uname' => 'fake_username',
'ldap_pword' => Crypt::encrypt("fake_password"),
'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com'
]);
}
public function enableBadPasswordLdap(): Settings
{
return $this->update([
'ldap_enabled' => 1,
'ldap_server' => 'ldaps://ldap.example.com',
'ldap_uname' => 'fake_username',
'ldap_pword' => "badly_encrypted_password!",
'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com'
]);
}
/**
* @param array $attributes Attributes to modify in the application's settings.
*/

View file

@ -46,7 +46,5 @@ class AssetMaintenanceTest extends TestCase
$this->assertTrue($c->completion_date === null);
$c->completion_date = '0000-00-00';
$this->assertTrue($c->completion_date === null);
$c->completion_date = '2017-05-12';
$this->assertTrue($c->completion_date == Carbon::parse('2017-05-12'));
}
}

210
tests/Unit/LdapTest.php Normal file
View file

@ -0,0 +1,210 @@
<?php
namespace Tests\Unit;
use App\Models\Ldap;
use Exception;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class LdapTest extends TestCase
{
use InteractsWithSettings;
use \phpmock\phpunit\PHPMock;
public function testConnect()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
$blah = Ldap::connectToLdap();
$this->assertEquals('hello',$blah,"LDAP_connect should return 'hello'");
}
// other test cases - with/without client-side certs?
// with/without LDAP version 3?
// with/without ignore cert validation?
// test (and mock) ldap_start_tls() ?
public function testBindAdmin()
{
$this->settings->enableLdap();
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->assertNull(Ldap::bindAdminToLdap("dummy"));
}
public function testBindBad()
{
$this->settings->enableLdap();
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(false);
$this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception");
$this->expectExceptionMessage("Could not bind to LDAP:");
$this->assertNull(Ldap::bindAdminToLdap("dummy"));
}
// other test cases - test donked password?
public function testAnonymousBind()
{
//todo - would be nice to introspect somehow to make sure the right parameters were passed?
$this->settings->enableAnonymousLdap();
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->assertNull(Ldap::bindAdminToLdap("dummy"));
}
public function testBadAnonymousBind()
{
$this->settings->enableAnonymousLdap();
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(false);
$this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception");
$this->expectExceptionMessage("Could not bind to LDAP:");
$this->assertNull(Ldap::bindAdminToLdap("dummy"));
}
public function testBadEncryptedPassword()
{
$this->settings->enableBadPasswordLdap();
$this->expectExceptionMessage("Your app key has changed");
$this->assertNull(Ldap::bindAdminToLdap("dummy"));
}
public function testFindAndBind()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_first_entry")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_get_attributes")->expects($this->once())->willReturn(
[
"count" => 1,
0 => [
'sn' => 'Surname',
'firstName' => 'FirstName'
]
]
);
$results = Ldap::findAndBindUserLdap("username","password");
$this->assertEqualsCanonicalizing(["count" =>1,0 =>['sn' => 'Surname','firstname' => 'FirstName']],$results);
}
public function testFindAndBindBadPassword()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
// note - we return FALSE first, to simulate a bad-bind, then TRUE the second time to simulate a successful admin bind
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->exactly(2))->willReturn(false, true);
// $this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception");
// $this->expectExceptionMessage("exception");
$results = Ldap::findAndBindUserLdap("username","password");
$this->assertFalse($results);
}
public function testFindAndBindCannotFindSelf()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(false);
$this->expectExceptionMessage("Could not search LDAP:");
$results = Ldap::findAndBindUserLdap("username","password");
$this->assertFalse($results);
}
//maybe should do an AD test as well?
public function testFindLdapUsers()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(["stuff"]);
$this->getFunctionMock("App\\Models", "ldap_parse_result")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_get_entries")->expects($this->once())->willReturn(["count" => 1]);
$results = Ldap::findLdapUsers();
$this->assertEqualsCanonicalizing(["count" => 1], $results);
}
public function testFindLdapUsersPaginated()
{
$this->settings->enableLdap();
$ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect");
$ldap_connect->expects($this->once())->willReturn('hello');
$ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option");
$ldap_set_option->expects($this->exactly(3));
$this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true);
$this->getFunctionMock("App\\Models", "ldap_search")->expects($this->exactly(2))->willReturn(["stuff"]);
$this->getFunctionMock("App\\Models", "ldap_parse_result")->expects($this->exactly(2))->willReturnCallback(
function ($ldapconn, $search_results, $errcode , $matcheddn , $errmsg , $referrals, &$controls) {
static $count = 0;
if($count == 0) {
$count++;
$controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] = "cookie";
return ["count" => 1];
} else {
$controls = [];
return ["count" => 1];
}
}
);
$this->getFunctionMock("App\\Models", "ldap_get_entries")->expects($this->exactly(2))->willReturn(["count" => 1]);
$results = Ldap::findLdapUsers();
$this->assertEqualsCanonicalizing(["count" => 2], $results);
}
}