Merge branch 'refs/heads/upstream/dev' into feature/sc-26415

This commit is contained in:
akemidx 2024-10-16 18:32:36 -04:00
commit 5cb940c2ee
87 changed files with 5697 additions and 1327 deletions

View file

@ -97,7 +97,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS # OPTIONAL: SECURITY HEADER SETTINGS
# -------------------------------------------- # --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.0.0.0/8 APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.16.0.0/12
ALLOW_IFRAMING=false ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin REFERRER_POLICY=same-origin
ENABLE_CSP=false ENABLE_CSP=false

View file

@ -84,7 +84,11 @@ Since the release of the JSON REST API, several third-party developers have been
### Contributing ### Contributing
Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview). Please refrain from submitting issues or pull requests generated by fully-automated tools. Maintainers reserve the right, at their sole discretion, to close such submissions and to block any account responsible for them.
Ideally, contributions should follow from a human-to-human discussion in the form of an issue.
Please see the complete documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.

View file

@ -137,23 +137,24 @@ class LdapSync extends Command
} }
/* Determine which location to assign users to by default. */ /* Determine which location to assign users to by default. */
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose $default_location = null;
if ($this->option('location') != '') { if ($this->option('location') != '') {
if ($location = Location::where('name', '=', $this->option('location'))->first()) { if ($default_location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed'); Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')'); Log::debug('Importing to '.$default_location->name.' ('.$default_location->id.')');
} }
} elseif ($this->option('location_id')) { } elseif ($this->option('location_id')) {
//TODO - figure out how or why this is an array?
foreach($this->option('location_id') as $location_id) { foreach($this->option('location_id') as $location_id) {
if ($location = Location::where('id', '=', $location_id)->first()) { if ($default_location = Location::where('id', '=', $location_id)->first()) {
Log::debug('Location ID ' . $location_id . ' passed'); Log::debug('Location ID ' . $location_id . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')'); Log::debug('Importing to '.$default_location->name.' ('.$default_location->id.')');
} }
} }
} }
if (! isset($location)) { if (!isset($default_location)) {
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.'); Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
} }
@ -243,6 +244,7 @@ class LdapSync extends Command
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? ''; $item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? ''; $item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? ''; $item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
$location = $default_location; //initially, set '$location' to the default_location (which may just be `null`)
// ONLY if you are using the "ldap_location" option *AND* you have an actual result // ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_map["location"] && $item['location']) { if ($ldap_map["location"] && $item['location']) {
@ -296,7 +298,7 @@ class LdapSync extends Command
$user->department_id = $department->id; $user->department_id = $department->id;
} }
if($ldap_map["location"] != null){ if($ldap_map["location"] != null){
$user->location_id = $location ? $location->id : null; $user->location_id = $location?->id;
} }
if($ldap_map["manager"] != null){ if($ldap_map["manager"] != null){
@ -399,9 +401,12 @@ class LdapSync extends Command
if ((is_array($location)) && (array_key_exists('id', $location))) { if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id']; $user->location_id = $location['id'];
} elseif (is_object($location)) { } elseif (is_object($location)) {
$user->location_id = $location->id; $user->location_id = $location->id; //THIS is the magic line, this should do it.
} }
} }
// TODO - should we be NULLING locations if $location is really `null`, and that's what we came up with?
// will that conflict with any overriding setting that the user set? Like, if they moved someone from
// the 'null' location to somewhere, we wouldn't want to try to override that, right?
$location = null; $location = null;
$user->ldap_import = 1; $user->ldap_import = 1;

View file

@ -137,7 +137,6 @@ class AccessoriesController extends Controller
*/ */
public function store(StoreAccessoryRequest $request) public function store(StoreAccessoryRequest $request)
{ {
$this->authorize('create', Accessory::class);
$accessory = new Accessory; $accessory = new Accessory;
$accessory->fill($request->all()); $accessory->fill($request->all());
$accessory = $request->handleImages($accessory); $accessory = $request->handleImages($accessory);
@ -197,9 +196,6 @@ class AccessoriesController extends Controller
$this->authorize('view', Accessory::class); $this->authorize('view', Accessory::class);
$accessory = Accessory::with('lastCheckout')->findOrFail($id); $accessory = Accessory::with('lastCheckout')->findOrFail($id);
if (! Company::isCurrentUserHasAccess($accessory)) {
return ['total' => 0, 'rows' => []];
}
$offset = request('offset', 0); $offset = request('offset', 0);
$limit = request('limit', 50); $limit = request('limit', 50);
@ -325,7 +321,7 @@ class AccessoriesController extends Controller
$accessory = Accessory::find($accessory_checkout->accessory_id); $accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory); $this->authorize('checkin', $accessory);
$logaction = $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note')); $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note'));
// Was the accessory updated? // Was the accessory updated?
if ($accessory_checkout->delete()) { if ($accessory_checkout->delete()) {
@ -333,14 +329,6 @@ class AccessoriesController extends Controller
$user = User::find($accessory_checkout->assigned_to); $user = User::find($accessory_checkout->assigned_to);
} }
$data['log_id'] = $logaction->id;
$data['first_name'] = $user->first_name;
$data['last_name'] = $user->last_name;
$data['item_name'] = $accessory->name;
$data['checkin_date'] = $logaction->created_at;
$data['item_tag'] = '';
$data['note'] = $logaction->note;
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
} }

View file

@ -395,7 +395,7 @@ class AssetsController extends Controller
// This may not work for all databases, but it works for MySQL // This may not work for all databases, but it works for MySQL
if ($numeric_sort) { if ($numeric_sort) {
$assets->orderByRaw($sort_override . ' * 1 ' . $order); $assets->orderByRaw(DB::getTablePrefix() . 'assets.' . $sort_override . ' * 1 ' . $order);
} else { } else {
$assets->orderBy($sort_override, $order); $assets->orderBy($sort_override, $order);
} }

View file

@ -42,13 +42,14 @@ class UsersController extends Controller
$users = User::select([ $users = User::select([
'users.activated', 'users.activated',
'users.created_by',
'users.address', 'users.address',
'users.avatar', 'users.avatar',
'users.city', 'users.city',
'users.company_id', 'users.company_id',
'users.country', 'users.country',
'users.created_by',
'users.created_at', 'users.created_at',
'users.updated_at',
'users.deleted_at', 'users.deleted_at',
'users.department_id', 'users.department_id',
'users.email', 'users.email',
@ -67,7 +68,6 @@ class UsersController extends Controller
'users.state', 'users.state',
'users.two_factor_enrolled', 'users.two_factor_enrolled',
'users.two_factor_optin', 'users.two_factor_optin',
'users.updated_at',
'users.username', 'users.username',
'users.zip', 'users.zip',
'users.remote', 'users.remote',
@ -255,6 +255,7 @@ class UsersController extends Controller
'groups', 'groups',
'activated', 'activated',
'created_at', 'created_at',
'updated_at',
'two_factor_enrolled', 'two_factor_enrolled',
'two_factor_optin', 'two_factor_optin',
'last_login', 'last_login',

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
/** /**
* This controller provide the health route for * This controller provide the health route for
@ -15,13 +16,35 @@ use Illuminate\Routing\Controller as BaseController;
*/ */
class HealthController extends BaseController class HealthController extends BaseController
{ {
public function __construct()
{
$this->middleware('health');
}
/** /**
* Returns a fixed JSON content ({ "status": "ok"}) which indicate the app is up and running * Returns a fixed JSON content ({ "status": "ok"}) which indicate the app is up and running
*/ */
public function get() public function get()
{ {
try {
if (DB::select('select 2 + 2')) {
return response()->json([ return response()->json([
'status' => 'ok', 'status' => 'ok',
]); ]);
}
} catch (\Exception $e) {
\Log::error('Could not connect to database');
return response()->json([
'status' => 'database connection failed',
], 500);
}
} }
} }

View file

@ -194,14 +194,14 @@ class ProfileController extends Controller
*/ */
public function printInventory() : View public function printInventory() : View
{ {
$show_user = auth()->user(); $show_users = User::where('id',auth()->user()->id)->get();
return view('users/print') return view('users/print')
->with('assets', auth()->user()->assets) ->with('assets', auth()->user()->assets())
->with('licenses', $show_user->licenses()->get()) ->with('licenses', auth()->user()->licenses()->get())
->with('accessories', $show_user->accessories()->get()) ->with('accessories', auth()->user()->accessories()->get())
->with('consumables', $show_user->consumables()->get()) ->with('consumables', auth()->user()->consumables()->get())
->with('show_user', $show_user) ->with('users', $show_users)
->with('settings', Setting::getSettings()); ->with('settings', Setting::getSettings());
} }
@ -222,7 +222,12 @@ class ProfileController extends Controller
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email')); return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
} }
try {
$user->notify((new CurrentInventory($user))); $user->notify((new CurrentInventory($user)));
} catch (\Exception $e) {
\Log::error($e);
}
return redirect()->back()->with('success', trans('admin/users/general.user_notified')); return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
} }
} }

View file

@ -7,6 +7,11 @@ use App\Helpers\StorageHelper;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\SettingsSamlRequest; use App\Http\Requests\SettingsSamlRequest;
use App\Http\Requests\SetupUserRequest; use App\Http\Requests\SetupUserRequest;
use App\Http\Requests\StoreLdapSettings;
use App\Http\Requests\StoreLocalizationSettings;
use App\Http\Requests\StoreNotificationSettings;
use App\Http\Requests\StoreLabelSettings;
use App\Http\Requests\StoreSecuritySettings;
use App\Models\CustomField; use App\Models\CustomField;
use App\Models\Group; use App\Models\Group;
use App\Models\Setting; use App\Models\Setting;
@ -273,20 +278,6 @@ class SettingsController extends Controller
return view('settings/index', compact('settings')); return view('settings/index', compact('settings'));
} }
/**
* Return the admin settings page.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v1.0]
*/
public function getEdit() : View
{
$setting = Setting::getSettings();
return view('settings/general', compact('setting'));
}
/** /**
* Return a form to allow a super admin to update settings. * Return a form to allow a super admin to update settings.
@ -488,7 +479,7 @@ class SettingsController extends Controller
* *
* @since [v1.0] * @since [v1.0]
*/ */
public function postSecurity(Request $request) : RedirectResponse public function postSecurity(StoreSecuritySettings $request) : RedirectResponse
{ {
$this->validate($request, [ $this->validate($request, [
'pwd_secure_complexity' => 'array', 'pwd_secure_complexity' => 'array',
@ -558,7 +549,7 @@ class SettingsController extends Controller
* *
* @since [v1.0] * @since [v1.0]
*/ */
public function postLocalization(Request $request) : RedirectResponse public function postLocalization(StoreLocalizationSettings $request) : RedirectResponse
{ {
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
@ -601,7 +592,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
*/ */
public function postAlerts(Request $request) : RedirectResponse public function postAlerts(StoreNotificationSettings $request) : RedirectResponse
{ {
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
@ -782,7 +773,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
*/ */
public function postLabels(Request $request) : RedirectResponse public function postLabels(StoreLabelSettings $request) : RedirectResponse
{ {
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
@ -861,26 +852,7 @@ class SettingsController extends Controller
{ {
$setting = Setting::getSettings(); $setting = Setting::getSettings();
$groups = Group::pluck('name', 'id'); $groups = Group::pluck('name', 'id');
return view('settings.ldap', compact('setting', 'groups'));
/**
* This validator is only temporary (famous last words.) - @snipe
*/
$messages = [
'ldap_username_field.not_in' => '<code>sAMAccountName</code> (mixed case) will likely not work. You should use <code>samaccountname</code> (lowercase) instead. ',
'ldap_auth_filter_query.not_in' => '<code>uid=samaccountname</code> is probably not a valid auth filter. You probably want <code>uid=</code> ',
'ldap_filter.regex' => 'This value should probably not be wrapped in parentheses.',
];
$validator = Validator::make($setting->toArray(), [
'ldap_username_field' => 'not_in:sAMAccountName',
'ldap_auth_filter_query' => 'not_in:uid=samaccountname|required_if:ldap_enabled,1',
'ldap_filter' => 'nullable|regex:"^[^(]"|required_if:ldap_enabled,1',
], $messages);
return view('settings.ldap', compact('setting', 'groups'))->withErrors($validator);
} }
/** /**
@ -889,7 +861,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
*/ */
public function postLdapSettings(Request $request) : RedirectResponse public function postLdapSettings(StoreLdapSettings $request) : RedirectResponse
{ {
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));

View file

@ -53,6 +53,10 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CheckLocale::class, \App\Http\Middleware\CheckLocale::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
'health' => [
],
]; ];
/** /**
@ -69,5 +73,6 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'health' => null,
]; ];
} }

View file

@ -7,14 +7,19 @@ use Closure;
class CheckForSetup class CheckForSetup
{ {
protected $except = [
'_debugbar*',
'health'
];
public function handle($request, Closure $next, $guard = null) public function handle($request, Closure $next, $guard = null)
{ {
/** /**
* This is dumb * Skip this middleware for the debugbar and health check
* @todo Check on removing this, not sure if it's still needed
*/ */
if ($request->is('_debugbar*')) { if ($request->is($this->except)) {
return $next($request); return $next($request);
} }
@ -25,7 +30,7 @@ class CheckForSetup
return $next($request); return $next($request);
} }
} else { } else {
if (! ($request->is('setup*')) && ! ($request->is('.env')) && ! ($request->is('health'))) { if (! ($request->is('setup*')) && ! ($request->is('.env'))) {
return redirect(config('app.url').'/setup'); return redirect(config('app.url').'/setup');
} }

View file

@ -26,18 +26,11 @@ class StoreAssetRequest extends ImageUploadRequest
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
// Guard against users passing in an array for company_id instead of an integer.
// If the company_id is not an integer then we simply use what was
// provided to be caught by model level validation later.
$idForCurrentUser = is_int($this->company_id)
? Company::getIdForCurrentUser($this->company_id)
: $this->company_id;
$this->parseLastAuditDate(); $this->parseLastAuditDate();
$this->merge([ $this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(), 'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser, 'company_id' => Company::getIdForCurrentUser($this->company_id),
'assigned_to' => $assigned_to ?? null, 'assigned_to' => $assigned_to ?? null,
]); ]);
} }

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreLabelSettings extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('superuser');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'labels_per_page' => 'numeric',
'labels_width' => 'numeric',
'labels_height' => 'numeric',
'labels_pmargin_left' => 'numeric|nullable',
'labels_pmargin_right' => 'numeric|nullable',
'labels_pmargin_top' => 'numeric|nullable',
'labels_pmargin_bottom' => 'numeric|nullable',
'labels_display_bgutter' => 'numeric|nullable',
'labels_display_sgutter' => 'numeric|nullable',
'labels_fontsize' => 'numeric|min:5',
'labels_pagewidth' => 'numeric|nullable',
'labels_pageheight' => 'numeric|nullable',
'qr_text' => 'max:31|nullable',
];
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreLdapSettings extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('superuser');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ldap_username_field' => 'not_in:sAMAccountName|required_if:ldap_enabled,1',
'ldap_auth_filter_query' => 'not_in:uid=samaccountname|required_if:ldap_enabled,1',
'ldap_filter' => 'nullable|regex:"^[^(]"|required_if:ldap_enabled,1',
'ldap_server' => 'nullable|required_if:ldap_enabled,1|starts_with:ldap://,ldaps://',
'ldap_uname' => 'nullable|required_if:ldap_enabled,1',
'ldap_pword' => 'nullable|required_if:ldap_enabled,1',
'ldap_basedn' => 'nullable|required_if:ldap_enabled,1',
'ldap_fname_field' => 'nullable|required_if:ldap_enabled,1',
'custom_forgot_pass_url' => 'nullable|url',
];
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreLocalizationSettings extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('superuser');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'default_currency' => 'required',
'locale' => 'required',
];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests;
use App\Models\Accessory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreNotificationSettings extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('superuser');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email|nullable',
'alert_threshold' => 'numeric|nullable|gt:0',
'alert_interval' => 'numeric|nullable|gt:0',
'audit_warning_days' => 'numeric|nullable|gt:0',
'due_checkin_days' => 'numeric|nullable|gt:0',
'audit_interval' => 'numeric|nullable|gt:0',
];
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreSecuritySettings extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('superuser');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'pwd_secure_min' => 'numeric|required|min:8',
'custom_forgot_pass_url' => 'url|nullable',
'privacy_policy_link' => 'nullable|url',
'login_remote_user_enabled' => 'numeric|nullable',
'login_common_disabled' => 'numeric|nullable',
'login_remote_user_custom_logout_url' => 'string|nullable',
'login_remote_user_header_name' => 'string|nullable',
];
}
}

View file

@ -66,7 +66,7 @@ class AssetMaintenancesTransformer
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'), 'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'user_id' => ($assetmaintenance->adminuser) ? [ 'user_id' => ($assetmaintenance->adminuser) ? [
'id' => $assetmaintenance->adminuser->id, 'id' => $assetmaintenance->adminuser->id,
'name'=> e($assetmaintenance->admin->getFullNameAttribute()) 'name'=> e($assetmaintenance->adminuser->present()->fullName())
] : null, // legacy to not change the shape of the API ] : null, // legacy to not change the shape of the API
'created_by' => ($assetmaintenance->adminuser) ? [ 'created_by' => ($assetmaintenance->adminuser) ? [
'id' => (int) $assetmaintenance->adminuser->id, 'id' => (int) $assetmaintenance->adminuser->id,

View file

@ -164,6 +164,7 @@ abstract class Importer
$this->log('------------- Action Summary ----------------'); $this->log('------------- Action Summary ----------------');
} }
Model::reguard();
}); });
} }

View file

@ -368,7 +368,7 @@ class Asset extends Depreciable
if ($this->save()) { if ($this->save()) {
if (is_int($admin)) { if (is_int($admin)) {
$checkedOutBy = User::findOrFail($admin); $checkedOutBy = User::findOrFail($admin);
} elseif (get_class($admin) === \App\Models\User::class) { } elseif ($admin && get_class($admin) === \App\Models\User::class) {
$checkedOutBy = $admin; $checkedOutBy = $admin;
} else { } else {
$checkedOutBy = auth()->user(); $checkedOutBy = auth()->user();

View file

@ -176,7 +176,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function adminuser() public function adminuser()
{ {
return $this->belongsTo(\App\Models\User::class, 'user_id') return $this->belongsTo(\App\Models\User::class, 'created_by')
->withTrashed(); ->withTrashed();
} }

View file

@ -2,10 +2,13 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Import extends Model class Import extends Model
{ {
use HasFactory;
protected $casts = [ protected $casts = [
'header_row' => 'array', 'header_row' => 'array',
'first_row' => 'array', 'first_row' => 'array',

View file

@ -42,7 +42,7 @@ class Location extends SnipeModel
]; ];
/** /**
* Whether the model should inject it's identifier to the unique * Whether the model should inject its identifier to the unique
* validation rules before attempting validation. If this property * validation rules before attempting validation. If this property
* is not set in the model it will default to true. * is not set in the model it will default to true.
* *

View file

@ -117,7 +117,6 @@ trait Loggable
*/ */
public function logCheckin($target, $note, $action_date = null, $originalValues = []) public function logCheckin($target, $note, $action_date = null, $originalValues = [])
{ {
$settings = Setting::getSettings();
$log = new Actionlog; $log = new Actionlog;
if($target != null){ if($target != null){
@ -171,39 +170,6 @@ trait Loggable
$log->logaction('checkin from'); $log->logaction('checkin from');
// $params = [
// 'target' => $target,
// 'item' => $log->item,
// 'admin' => $log->user,
// 'note' => $note,
// 'target_type' => $log->target_type,
// 'settings' => $settings,
// ];
//
//
// $checkinClass = null;
//
// if (method_exists($target, 'notify')) {
// try {
// $target->notify(new static::$checkinClass($params));
// } catch (\Exception $e) {
// Log::debug($e);
// }
//
// }
//
// // Send to the admin, if settings dictate
// $recipient = new \App\Models\Recipients\AdminRecipient();
//
// if (($settings->admin_cc_email!='') && (static::$checkinClass!='')) {
// try {
// $recipient->notify(new static::$checkinClass($params));
// } catch (\Exception $e) {
// Log::debug($e);
// }
//
// }
return $log; return $log;
} }

View file

@ -51,36 +51,7 @@ class Setting extends Model
*/ */
protected $rules = [ protected $rules = [
'brand' => 'required|min:1|numeric', 'brand' => 'required|min:1|numeric',
'qr_text' => 'max:31|nullable',
'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email|nullable',
'default_currency' => 'required',
'locale' => 'required',
'labels_per_page' => 'numeric',
'labels_width' => 'numeric',
'labels_height' => 'numeric',
'labels_pmargin_left' => 'numeric|nullable',
'labels_pmargin_right' => 'numeric|nullable',
'labels_pmargin_top' => 'numeric|nullable',
'labels_pmargin_bottom' => 'numeric|nullable',
'labels_display_bgutter' => 'numeric|nullable',
'labels_display_sgutter' => 'numeric|nullable',
'labels_fontsize' => 'numeric|min:5',
'labels_pagewidth' => 'numeric|nullable',
'labels_pageheight' => 'numeric|nullable',
'login_remote_user_enabled' => 'numeric|nullable',
'login_common_disabled' => 'numeric|nullable',
'login_remote_user_custom_logout_url' => 'string|nullable',
'login_remote_user_header_name' => 'string|nullable',
'thumbnail_max_h' => 'numeric|max:500|min:25', 'thumbnail_max_h' => 'numeric|max:500|min:25',
'pwd_secure_min' => 'numeric|required|min:8',
'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable',
'audit_warning_days' => 'numeric|nullable',
'due_checkin_days' => 'numeric|nullable',
'audit_interval' => 'numeric|nullable',
'custom_forgot_pass_url' => 'url|nullable',
'privacy_policy_link' => 'nullable|url',
'google_client_id' => 'nullable|ends_with:apps.googleusercontent.com' 'google_client_id' => 'nullable|ends_with:apps.googleusercontent.com'
]; ];

View file

@ -116,12 +116,6 @@ class AssetMaintenancesPresenter extends Presenter
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.cost'), 'title' => trans('admin/asset_maintenances/form.cost'),
'class' => 'text-right', 'class' => 'text-right',
], [
'field' => 'user_id',
'searchable' => true,
'sortable' => true,
'title' => trans('general.admin'),
'formatter' => 'usersLinkObjFormatter',
], [ ], [
'field' => 'created_by', 'field' => 'created_by',
'searchable' => false, 'searchable' => false,

View file

@ -31,6 +31,7 @@ class ValidationServiceProvider extends ServiceProvider
Validator::extend('email_array', function ($attribute, $value, $parameters, $validator) { Validator::extend('email_array', function ($attribute, $value, $parameters, $validator) {
$value = str_replace(' ', '', $value); $value = str_replace(' ', '', $value);
$array = explode(',', $value); $array = explode(',', $value);
$email_to_validate = [];
foreach ($array as $email) { //loop over values foreach ($array as $email) { //loop over values
$email_to_validate['alert_email'][] = $email; $email_to_validate['alert_email'][] = $email;
@ -38,7 +39,7 @@ class ValidationServiceProvider extends ServiceProvider
$rules = ['alert_email.*'=>'email']; $rules = ['alert_email.*'=>'email'];
$messages = [ $messages = [
'alert_email.*'=>trans('validation.email_array'), 'alert_email.*' => trans('validation.custom.email_array'),
]; ];
$validator = Validator::make($email_to_validate, $rules, $messages); $validator = Validator::make($email_to_validate, $rules, $messages);

1297
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Category; use App\Models\Category;
use App\Models\Location; use App\Models\Location;
use App\Models\Manufacturer; use App\Models\Manufacturer;
@ -156,4 +155,19 @@ class AccessoryFactory extends Factory
]); ]);
}); });
} }
public function checkedOutToUsers(array $users)
{
return $this->afterCreating(function (Accessory $accessory) use ($users) {
foreach ($users as $user) {
$accessory->checkouts()->create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => 1,
'assigned_to' => $user->id,
'assigned_type' => User::class,
]);
}
});
}
} }

View file

@ -0,0 +1,146 @@
<?php
namespace Database\Factories;
use App\Models\Import;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Support\Importing;
/**
* @extends Factory<Import>
*/
class ImportFactory extends Factory
{
/**
* @inheritdoc
*/
protected $model = Import::class;
/**
* @inheritdoc
*/
public function definition()
{
return [
'name' => $this->faker->company,
'file_path' => Str::random().'.csv',
'filesize' => $this->faker->randomDigitNotNull(),
'field_map' => null,
];
}
/**
* Create an accessory import type.
*
* @return static
*/
public function accessory()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\AccessoriesImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Accessories";
$attributes['import_type'] = 'accessory';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
/**
* Create an asset import type.
*
* @return static
*/
public function asset()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\AssetsImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Assets";
$attributes['import_type'] = 'asset';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
/**
* Create a component import type.
*
* @return static
*/
public function component()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\ComponentsImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Components";
$attributes['import_type'] = 'component';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
/**
* Create a consumable import type.
*
* @return static
*/
public function consumable()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\ConsumablesImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Consumables";
$attributes['import_type'] = 'consumable';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
/**
* Create a license import type.
*
* @return static
*/
public function license()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\LicensesImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Licenses";
$attributes['import_type'] = 'license';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
/**
* Create a users import type.
*
* @return static
*/
public function users()
{
return $this->state(function (array $attributes) {
$fileBuilder = Importing\UsersImportFileBuilder::new();
$attributes['name'] = "{$attributes['name']} Employees";
$attributes['import_type'] = 'user';
$attributes['header_row'] = $fileBuilder->toCsv()[0];
$attributes['first_row'] = $fileBuilder->firstRow();
return $attributes;
});
}
}

View file

@ -7,6 +7,9 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use \Auth; use \Auth;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory class UserFactory extends Factory
{ {
/** /**

View file

@ -942,20 +942,21 @@ h4 {
background-color: #f9f9f9; background-color: #f9f9f9;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-striped .row:nth-of-type(even) div { .row-striped .row:nth-of-type(even) div {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-new-striped { .row-new-striped {
vertical-align: top; vertical-align: top;
line-height: 2.6; padding: 3px;
padding: 0px;
margin-left: 20px;
display: table; display: table;
width: 100%; width: 100%;
padding-right: 20px; word-wrap: break-word;
table-layout: fixed;
} }
/** /**
* NEW STRIPING * NEW STRIPING
@ -965,20 +966,25 @@ h4 {
.row-new-striped > .row:nth-of-type(even) { .row-new-striped > .row:nth-of-type(even) {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row; display: table-row;
} }
.row-new-striped > .row:nth-of-type(odd) { .row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8; background-color: #F8F8F8;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-row; display: table-row;
line-height: 1.9;
padding: 2px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div[class^="col"]:first-child { .row-new-striped div[class^="col"]:first-child {
font-weight: bold; font-weight: bold;

View file

@ -574,20 +574,21 @@ h4 {
background-color: #f9f9f9; background-color: #f9f9f9;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-striped .row:nth-of-type(even) div { .row-striped .row:nth-of-type(even) div {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-new-striped { .row-new-striped {
vertical-align: top; vertical-align: top;
line-height: 2.6; padding: 3px;
padding: 0px;
margin-left: 20px;
display: table; display: table;
width: 100%; width: 100%;
padding-right: 20px; word-wrap: break-word;
table-layout: fixed;
} }
/** /**
* NEW STRIPING * NEW STRIPING
@ -597,20 +598,25 @@ h4 {
.row-new-striped > .row:nth-of-type(even) { .row-new-striped > .row:nth-of-type(even) {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row; display: table-row;
} }
.row-new-striped > .row:nth-of-type(odd) { .row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8; background-color: #F8F8F8;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-row; display: table-row;
line-height: 1.9;
padding: 2px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div[class^="col"]:first-child { .row-new-striped div[class^="col"]:first-child {
font-weight: bold; font-weight: bold;

View file

@ -21914,20 +21914,21 @@ h4 {
background-color: #f9f9f9; background-color: #f9f9f9;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-striped .row:nth-of-type(even) div { .row-striped .row:nth-of-type(even) div {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-new-striped { .row-new-striped {
vertical-align: top; vertical-align: top;
line-height: 2.6; padding: 3px;
padding: 0px;
margin-left: 20px;
display: table; display: table;
width: 100%; width: 100%;
padding-right: 20px; word-wrap: break-word;
table-layout: fixed;
} }
/** /**
* NEW STRIPING * NEW STRIPING
@ -21937,20 +21938,25 @@ h4 {
.row-new-striped > .row:nth-of-type(even) { .row-new-striped > .row:nth-of-type(even) {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row; display: table-row;
} }
.row-new-striped > .row:nth-of-type(odd) { .row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8; background-color: #F8F8F8;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-row; display: table-row;
line-height: 1.9;
padding: 2px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div[class^="col"]:first-child { .row-new-striped div[class^="col"]:first-child {
font-weight: bold; font-weight: bold;
@ -23389,20 +23395,21 @@ h4 {
background-color: #f9f9f9; background-color: #f9f9f9;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-striped .row:nth-of-type(even) div { .row-striped .row:nth-of-type(even) div {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-new-striped { .row-new-striped {
vertical-align: top; vertical-align: top;
line-height: 2.6; padding: 3px;
padding: 0px;
margin-left: 20px;
display: table; display: table;
width: 100%; width: 100%;
padding-right: 20px; word-wrap: break-word;
table-layout: fixed;
} }
/** /**
* NEW STRIPING * NEW STRIPING
@ -23412,20 +23419,25 @@ h4 {
.row-new-striped > .row:nth-of-type(even) { .row-new-striped > .row:nth-of-type(even) {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row; display: table-row;
} }
.row-new-striped > .row:nth-of-type(odd) { .row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8; background-color: #F8F8F8;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-row; display: table-row;
line-height: 1.9;
padding: 2px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div[class^="col"]:first-child { .row-new-striped div[class^="col"]:first-child {
font-weight: bold; font-weight: bold;

View file

@ -2,8 +2,8 @@
"/js/build/app.js": "/js/build/app.js?id=5e9ac5c1a7e089f056fb1dba566193a6", "/js/build/app.js": "/js/build/app.js?id=5e9ac5c1a7e089f056fb1dba566193a6",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f0b08873a06bb54daeee176a9459f4a9", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f0b08873a06bb54daeee176a9459f4a9",
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=f4397c717b99fce41a633ca6edd5d1f4", "/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=f4397c717b99fce41a633ca6edd5d1f4",
"/css/build/overrides.css": "/css/build/overrides.css?id=efd9f439cb0586512d03172bcd9a5752", "/css/build/overrides.css": "/css/build/overrides.css?id=1c3ffc5fb379e21523f2a9b03f986edb",
"/css/build/app.css": "/css/build/app.css?id=2f45befb40b9d7f038eeae9569c33a5f", "/css/build/app.css": "/css/build/app.css?id=d04f32982fb319ac35a32d362089f18b",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=393aaa7b368b0670fc42434c8cca7dc7", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=393aaa7b368b0670fc42434c8cca7dc7",
@ -19,7 +19,7 @@
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=0640e45bad692dcf62873c6e85904899", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=0640e45bad692dcf62873c6e85904899",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/all.css": "/css/dist/all.css?id=e9509d7591637153f667461642e47e30", "/css/dist/all.css": "/css/dist/all.css?id=9f69886d7a8e4c383cd09a48573922b7",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/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/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",

View file

@ -1432,11 +1432,11 @@ var require_module_cjs = __commonJS({
}); });
} }
function cleanupElement(el) { function cleanupElement(el) {
if (el._x_cleanups) { var _a, _b;
while (el._x_cleanups.length) (_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);
while ((_b = el._x_cleanups) == null ? void 0 : _b.length)
el._x_cleanups.pop()(); el._x_cleanups.pop()();
} }
}
var observer = new MutationObserver(onMutate); var observer = new MutationObserver(onMutate);
var currentlyObserving = false; var currentlyObserving = false;
function startObservingMutations() { function startObservingMutations() {
@ -1673,27 +1673,23 @@ var require_module_cjs = __commonJS({
magics[name] = callback; magics[name] = callback;
} }
function injectMagics(obj, el) { function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
Object.entries(magics).forEach(([name, callback]) => { Object.entries(magics).forEach(([name, callback]) => {
let memoizedUtilities = null;
function getUtilities() {
if (memoizedUtilities) {
return memoizedUtilities;
} else {
let [utilities, cleanup] = getElementBoundUtilities(el);
memoizedUtilities = { interceptor, ...utilities };
onElRemoved(el, cleanup);
return memoizedUtilities;
}
}
Object.defineProperty(obj, `$${name}`, { Object.defineProperty(obj, `$${name}`, {
get() { get() {
return callback(el, getUtilities()); return callback(el, memoizedUtilities);
}, },
enumerable: false enumerable: false
}); });
}); });
return obj; return obj;
} }
function getUtilities(el) {
let [utilities, cleanup] = getElementBoundUtilities(el);
let utils = { interceptor, ...utilities };
onElRemoved(el, cleanup);
return utils;
}
function tryCatch(el, expression, callback, ...args) { function tryCatch(el, expression, callback, ...args) {
try { try {
return callback(...args); return callback(...args);
@ -2067,8 +2063,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
function destroyTree(root, walker = walk) { function destroyTree(root, walker = walk) {
walker(root, (el) => { walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el); cleanupElement(el);
cleanupAttributes(el);
}); });
} }
function warnAboutMissingPlugins() { function warnAboutMissingPlugins() {
@ -2648,34 +2644,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
return rawValue ? Boolean(rawValue) : null; return rawValue ? Boolean(rawValue) : null;
} }
function isBooleanAttr(attrName) { var booleanAttributes = /* @__PURE__ */ new Set([
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen", "allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async", "async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer", "defer",
"nomodule" "disabled",
]; "formnovalidate",
return booleanAttributes.includes(attrName); "inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
return booleanAttributes.has(attrName);
} }
function attributeShouldntBePreservedIfFalsy(name) { function attributeShouldntBePreservedIfFalsy(name) {
return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name); return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name);
@ -2776,10 +2775,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return stores[name]; return stores[name];
} }
stores[name] = value; stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") { if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init(); stores[name].init();
} }
initInterceptors(stores[name]);
} }
function getStores() { function getStores() {
return stores; return stores;
@ -3070,7 +3069,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers); placeInDom(el._x_teleport, target2, modifiers);
}); });
}; };
cleanup(() => clone2.remove()); cleanup(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
}); });
var teleportContainerDuringClone = document.createElement("div"); var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) { function getTarget(expression) {
@ -3558,7 +3560,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {}; el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey)); effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
cleanup(() => { cleanup(() => {
Object.values(el._x_lookup).forEach((el2) => el2.remove()); Object.values(el._x_lookup).forEach((el2) => mutateDom(() => {
destroyTree(el2);
el2.remove();
}));
delete el._x_prevKeys; delete el._x_prevKeys;
delete el._x_lookup; delete el._x_lookup;
}); });
@ -3627,11 +3632,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
for (let i = 0; i < removes.length; i++) { for (let i = 0; i < removes.length; i++) {
let key = removes[i]; let key = removes[i];
if (!!lookup[key]._x_effects) { if (!(key in lookup))
lookup[key]._x_effects.forEach(dequeueJob); continue;
} mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove(); lookup[key].remove();
lookup[key] = null; });
delete lookup[key]; delete lookup[key];
} }
for (let i = 0; i < moves.length; i++) { for (let i = 0; i < moves.length; i++) {
@ -3752,12 +3758,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}); });
el._x_currentIfEl = clone2; el._x_currentIfEl = clone2;
el._x_undoIf = () => { el._x_undoIf = () => {
walk(clone2, (node) => { mutateDom(() => {
if (!!node._x_effects) { destroyTree(clone2);
node._x_effects.forEach(dequeueJob);
}
});
clone2.remove(); clone2.remove();
});
delete el._x_currentIfEl; delete el._x_currentIfEl;
}; };
return clone2; return clone2;
@ -3812,9 +3816,9 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
}); });
// ../alpine/packages/collapse/dist/module.cjs.js // ../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.cjs.js
var require_module_cjs2 = __commonJS({ var require_module_cjs2 = __commonJS({
"../alpine/packages/collapse/dist/module.cjs.js"(exports, module) { "../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -3887,7 +3891,7 @@ var require_module_cjs2 = __commonJS({
start: { height: current + "px" }, start: { height: current + "px" },
end: { height: full + "px" } end: { height: full + "px" }
}, () => el._x_isShown = true, () => { }, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) { if (el.getBoundingClientRect().height == full) {
el.style.overflow = null; el.style.overflow = null;
} }
}); });
@ -3933,9 +3937,9 @@ var require_module_cjs2 = __commonJS({
} }
}); });
// ../alpine/packages/focus/dist/module.cjs.js // ../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.cjs.js
var require_module_cjs3 = __commonJS({ var require_module_cjs3 = __commonJS({
"../alpine/packages/focus/dist/module.cjs.js"(exports, module) { "../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.cjs.js"(exports, module) {
var __create2 = Object.create; var __create2 = Object.create;
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -4935,9 +4939,9 @@ var require_module_cjs3 = __commonJS({
} }
}); });
// ../alpine/packages/persist/dist/module.cjs.js // ../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.cjs.js
var require_module_cjs4 = __commonJS({ var require_module_cjs4 = __commonJS({
"../alpine/packages/persist/dist/module.cjs.js"(exports, module) { "../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -5024,9 +5028,9 @@ var require_module_cjs4 = __commonJS({
} }
}); });
// ../alpine/packages/intersect/dist/module.cjs.js // ../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.cjs.js
var require_module_cjs5 = __commonJS({ var require_module_cjs5 = __commonJS({
"../alpine/packages/intersect/dist/module.cjs.js"(exports, module) { "../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -5106,8 +5110,80 @@ var require_module_cjs5 = __commonJS({
} }
}); });
// ../alpine/packages/anchor/dist/module.cjs.js // node_modules/@alpinejs/resize/dist/module.cjs.js
var require_module_cjs6 = __commonJS({ var require_module_cjs6 = __commonJS({
"node_modules/@alpinejs/resize/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
var __export = (target, all2) => {
for (var name in all2)
__defProp2(target, name, { get: all2[name], enumerable: true });
};
var __copyProps2 = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames2(from))
if (!__hasOwnProp2.call(to, key) && key !== except)
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
var module_exports = {};
__export(module_exports, {
default: () => module_default,
resize: () => src_default
});
module.exports = __toCommonJS(module_exports);
function src_default(Alpine19) {
Alpine19.directive("resize", Alpine19.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater, cleanup }) => {
let evaluator = evaluateLater(expression);
let evaluate = (width, height) => {
evaluator(() => {
}, { scope: { "$width": width, "$height": height } });
};
let off = modifiers.includes("document") ? onDocumentResize(evaluate) : onElResize(el, evaluate);
cleanup(() => off());
}));
}
function onElResize(el, callback) {
let observer = new ResizeObserver((entries) => {
let [width, height] = dimensions(entries);
callback(width, height);
});
observer.observe(el);
return () => observer.disconnect();
}
var documentResizeObserver;
var documentResizeObserverCallbacks = /* @__PURE__ */ new Set();
function onDocumentResize(callback) {
documentResizeObserverCallbacks.add(callback);
if (!documentResizeObserver) {
documentResizeObserver = new ResizeObserver((entries) => {
let [width, height] = dimensions(entries);
documentResizeObserverCallbacks.forEach((i) => i(width, height));
});
documentResizeObserver.observe(document.documentElement);
}
return () => {
documentResizeObserverCallbacks.delete(callback);
};
}
function dimensions(entries) {
let width, height;
for (let entry of entries) {
width = entry.borderBoxSize[0].inlineSize;
height = entry.borderBoxSize[0].blockSize;
}
return [width, height];
}
var module_default = src_default;
}
});
// ../alpine/packages/anchor/dist/module.cjs.js
var require_module_cjs7 = __commonJS({
"../alpine/packages/anchor/dist/module.cjs.js"(exports, module) { "../alpine/packages/anchor/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -6645,7 +6721,7 @@ var require_nprogress = __commonJS({
}); });
// ../alpine/packages/morph/dist/module.cjs.js // ../alpine/packages/morph/dist/module.cjs.js
var require_module_cjs7 = __commonJS({ var require_module_cjs8 = __commonJS({
"../alpine/packages/morph/dist/module.cjs.js"(exports, module) { "../alpine/packages/morph/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -6744,6 +6820,8 @@ var require_module_cjs7 = __commonJS({
let toAttributes = Array.from(to.attributes); let toAttributes = Array.from(to.attributes);
for (let i = domAttributes.length - 1; i >= 0; i--) { for (let i = domAttributes.length - 1; i >= 0; i--) {
let name = domAttributes[i].name; let name = domAttributes[i].name;
if (name === "style")
continue;
if (!to.hasAttribute(name)) { if (!to.hasAttribute(name)) {
from2.removeAttribute(name); from2.removeAttribute(name);
} }
@ -6751,6 +6829,8 @@ var require_module_cjs7 = __commonJS({
for (let i = toAttributes.length - 1; i >= 0; i--) { for (let i = toAttributes.length - 1; i >= 0; i--) {
let name = toAttributes[i].name; let name = toAttributes[i].name;
let value = toAttributes[i].value; let value = toAttributes[i].value;
if (name === "style")
continue;
if (from2.getAttribute(name) !== value) { if (from2.getAttribute(name) !== value) {
from2.setAttribute(name, value); from2.setAttribute(name, value);
} }
@ -7006,9 +7086,9 @@ var require_module_cjs7 = __commonJS({
} }
}); });
// ../alpine/packages/mask/dist/module.cjs.js // ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js
var require_module_cjs8 = __commonJS({ var require_module_cjs9 = __commonJS({
"../alpine/packages/mask/dist/module.cjs.js"(exports, module) { "../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -8509,7 +8589,8 @@ var import_collapse = __toESM(require_module_cjs2());
var import_focus = __toESM(require_module_cjs3()); var import_focus = __toESM(require_module_cjs3());
var import_persist2 = __toESM(require_module_cjs4()); var import_persist2 = __toESM(require_module_cjs4());
var import_intersect = __toESM(require_module_cjs5()); var import_intersect = __toESM(require_module_cjs5());
var import_anchor = __toESM(require_module_cjs6()); var import_resize = __toESM(require_module_cjs6());
var import_anchor = __toESM(require_module_cjs7());
// js/plugins/navigate/history.js // js/plugins/navigate/history.js
var Snapshot = class { var Snapshot = class {
@ -8660,7 +8741,7 @@ function extractDestinationFromLink(linkEl) {
return createUrlObjectFromString(linkEl.getAttribute("href")); return createUrlObjectFromString(linkEl.getAttribute("href"));
} }
function createUrlObjectFromString(urlString) { function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI); return urlString !== null && new URL(urlString, document.baseURI);
} }
function getUriStringFromUrlObject(urlObject) { function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash; return urlObject.pathname + urlObject.search + urlObject.hash;
@ -8781,10 +8862,12 @@ function restoreScrollPositionOrScrollToTop() {
el.removeAttribute("data-scroll-y"); el.removeAttribute("data-scroll-y");
} }
}; };
queueMicrotask(() => {
queueMicrotask(() => { queueMicrotask(() => {
scroll(document.body); scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll); document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
}); });
});
} }
// js/plugins/navigate/persist.js // js/plugins/navigate/persist.js
@ -9087,12 +9170,16 @@ function navigate_default(Alpine19) {
let shouldPrefetchOnHover = modifiers.includes("hover"); let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => { shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el); let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => { prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination); storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
}); });
}); });
whenThisLinkIsPressed(el, (whenItIsReleased) => { whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el); let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => { prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination); storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
}); });
@ -9441,8 +9528,8 @@ function fromQueryString(search) {
} }
// js/lifecycle.js // js/lifecycle.js
var import_morph = __toESM(require_module_cjs7()); var import_morph = __toESM(require_module_cjs8());
var import_mask = __toESM(require_module_cjs8()); var import_mask = __toESM(require_module_cjs9());
var import_alpinejs5 = __toESM(require_module_cjs()); var import_alpinejs5 = __toESM(require_module_cjs());
function start() { function start() {
setTimeout(() => ensureLivewireScriptIsntMisplaced()); setTimeout(() => ensureLivewireScriptIsntMisplaced());
@ -9451,6 +9538,7 @@ function start() {
import_alpinejs5.default.plugin(import_morph.default); import_alpinejs5.default.plugin(import_morph.default);
import_alpinejs5.default.plugin(history2); import_alpinejs5.default.plugin(history2);
import_alpinejs5.default.plugin(import_intersect.default); import_alpinejs5.default.plugin(import_intersect.default);
import_alpinejs5.default.plugin(import_resize.default);
import_alpinejs5.default.plugin(import_collapse.default); import_alpinejs5.default.plugin(import_collapse.default);
import_alpinejs5.default.plugin(import_anchor.default); import_alpinejs5.default.plugin(import_anchor.default);
import_alpinejs5.default.plugin(import_focus.default); import_alpinejs5.default.plugin(import_focus.default);

View file

@ -851,11 +851,10 @@
}); });
} }
function cleanupElement(el) { function cleanupElement(el) {
if (el._x_cleanups) { el._x_effects?.forEach(dequeueJob);
while (el._x_cleanups.length) while (el._x_cleanups?.length)
el._x_cleanups.pop()(); el._x_cleanups.pop()();
} }
}
var observer = new MutationObserver(onMutate); var observer = new MutationObserver(onMutate);
var currentlyObserving = false; var currentlyObserving = false;
function startObservingMutations() { function startObservingMutations() {
@ -1092,27 +1091,23 @@
magics[name] = callback; magics[name] = callback;
} }
function injectMagics(obj, el) { function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
Object.entries(magics).forEach(([name, callback]) => { Object.entries(magics).forEach(([name, callback]) => {
let memoizedUtilities = null;
function getUtilities() {
if (memoizedUtilities) {
return memoizedUtilities;
} else {
let [utilities, cleanup2] = getElementBoundUtilities(el);
memoizedUtilities = { interceptor, ...utilities };
onElRemoved(el, cleanup2);
return memoizedUtilities;
}
}
Object.defineProperty(obj, `$${name}`, { Object.defineProperty(obj, `$${name}`, {
get() { get() {
return callback(el, getUtilities()); return callback(el, memoizedUtilities);
}, },
enumerable: false enumerable: false
}); });
}); });
return obj; return obj;
} }
function getUtilities(el) {
let [utilities, cleanup2] = getElementBoundUtilities(el);
let utils = { interceptor, ...utilities };
onElRemoved(el, cleanup2);
return utils;
}
function tryCatch(el, expression, callback, ...args) { function tryCatch(el, expression, callback, ...args) {
try { try {
return callback(...args); return callback(...args);
@ -1486,8 +1481,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
function destroyTree(root, walker = walk) { function destroyTree(root, walker = walk) {
walker(root, (el) => { walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el); cleanupElement(el);
cleanupAttributes(el);
}); });
} }
function warnAboutMissingPlugins() { function warnAboutMissingPlugins() {
@ -2067,34 +2062,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
return rawValue ? Boolean(rawValue) : null; return rawValue ? Boolean(rawValue) : null;
} }
function isBooleanAttr(attrName) { var booleanAttributes = /* @__PURE__ */ new Set([
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen", "allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async", "async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer", "defer",
"nomodule" "disabled",
]; "formnovalidate",
return booleanAttributes.includes(attrName); "inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
return booleanAttributes.has(attrName);
} }
function attributeShouldntBePreservedIfFalsy(name) { function attributeShouldntBePreservedIfFalsy(name) {
return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name); return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name);
@ -2195,10 +2193,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return stores[name]; return stores[name];
} }
stores[name] = value; stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") { if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init(); stores[name].init();
} }
initInterceptors(stores[name]);
} }
function getStores() { function getStores() {
return stores; return stores;
@ -3136,7 +3134,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers); placeInDom(el._x_teleport, target2, modifiers);
}); });
}; };
cleanup2(() => clone2.remove()); cleanup2(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
}); });
var teleportContainerDuringClone = document.createElement("div"); var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) { function getTarget(expression) {
@ -3624,7 +3625,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {}; el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey)); effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
cleanup2(() => { cleanup2(() => {
Object.values(el._x_lookup).forEach((el2) => el2.remove()); Object.values(el._x_lookup).forEach((el2) => mutateDom(() => {
destroyTree(el2);
el2.remove();
}));
delete el._x_prevKeys; delete el._x_prevKeys;
delete el._x_lookup; delete el._x_lookup;
}); });
@ -3693,11 +3697,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
for (let i = 0; i < removes.length; i++) { for (let i = 0; i < removes.length; i++) {
let key = removes[i]; let key = removes[i];
if (!!lookup[key]._x_effects) { if (!(key in lookup))
lookup[key]._x_effects.forEach(dequeueJob); continue;
} mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove(); lookup[key].remove();
lookup[key] = null; });
delete lookup[key]; delete lookup[key];
} }
for (let i = 0; i < moves.length; i++) { for (let i = 0; i < moves.length; i++) {
@ -3818,12 +3823,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}); });
el._x_currentIfEl = clone2; el._x_currentIfEl = clone2;
el._x_undoIf = () => { el._x_undoIf = () => {
walk(clone2, (node) => { mutateDom(() => {
if (!!node._x_effects) { destroyTree(clone2);
node._x_effects.forEach(dequeueJob);
}
});
clone2.remove(); clone2.remove();
});
delete el._x_currentIfEl; delete el._x_currentIfEl;
}; };
return clone2; return clone2;
@ -4762,7 +4765,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
}; };
// ../alpine/packages/collapse/dist/module.esm.js // ../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.esm.js
function src_default2(Alpine3) { function src_default2(Alpine3) {
Alpine3.directive("collapse", collapse); Alpine3.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => { collapse.inline = (el, { modifiers }) => {
@ -4812,7 +4815,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
start: { height: current + "px" }, start: { height: current + "px" },
end: { height: full + "px" } end: { height: full + "px" }
}, () => el._x_isShown = true, () => { }, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) { if (el.getBoundingClientRect().height == full) {
el.style.overflow = null; el.style.overflow = null;
} }
}); });
@ -4856,7 +4859,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
var module_default2 = src_default2; var module_default2 = src_default2;
// ../alpine/packages/focus/dist/module.esm.js // ../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.esm.js
var candidateSelectors = ["input", "select", "textarea", "a[href]", "button", "[tabindex]:not(slot)", "audio[controls]", "video[controls]", '[contenteditable]:not([contenteditable="false"])', "details>summary:first-of-type", "details"]; var candidateSelectors = ["input", "select", "textarea", "a[href]", "button", "[tabindex]:not(slot)", "audio[controls]", "video[controls]", '[contenteditable]:not([contenteditable="false"])', "details>summary:first-of-type", "details"];
var candidateSelector = /* @__PURE__ */ candidateSelectors.join(","); var candidateSelector = /* @__PURE__ */ candidateSelectors.join(",");
var NoElement = typeof Element === "undefined"; var NoElement = typeof Element === "undefined";
@ -5805,7 +5808,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
var module_default3 = src_default3; var module_default3 = src_default3;
// ../alpine/packages/persist/dist/module.esm.js // ../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.esm.js
function src_default4(Alpine3) { function src_default4(Alpine3) {
let persist = () => { let persist = () => {
let alias; let alias;
@ -5867,7 +5870,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
var module_default4 = src_default4; var module_default4 = src_default4;
// ../alpine/packages/intersect/dist/module.esm.js // ../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.esm.js
function src_default5(Alpine3) { function src_default5(Alpine3) {
Alpine3.directive("intersect", Alpine3.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater: evaluateLater2, cleanup: cleanup2 }) => { Alpine3.directive("intersect", Alpine3.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let evaluate3 = evaluateLater2(expression); let evaluate3 = evaluateLater2(expression);
@ -5922,6 +5925,51 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
} }
var module_default5 = src_default5; var module_default5 = src_default5;
// node_modules/@alpinejs/resize/dist/module.esm.js
function src_default6(Alpine3) {
Alpine3.directive("resize", Alpine3.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let evaluator = evaluateLater2(expression);
let evaluate3 = (width, height) => {
evaluator(() => {
}, { scope: { "$width": width, "$height": height } });
};
let off = modifiers.includes("document") ? onDocumentResize(evaluate3) : onElResize(el, evaluate3);
cleanup2(() => off());
}));
}
function onElResize(el, callback) {
let observer2 = new ResizeObserver((entries) => {
let [width, height] = dimensions(entries);
callback(width, height);
});
observer2.observe(el);
return () => observer2.disconnect();
}
var documentResizeObserver;
var documentResizeObserverCallbacks = /* @__PURE__ */ new Set();
function onDocumentResize(callback) {
documentResizeObserverCallbacks.add(callback);
if (!documentResizeObserver) {
documentResizeObserver = new ResizeObserver((entries) => {
let [width, height] = dimensions(entries);
documentResizeObserverCallbacks.forEach((i) => i(width, height));
});
documentResizeObserver.observe(document.documentElement);
}
return () => {
documentResizeObserverCallbacks.delete(callback);
};
}
function dimensions(entries) {
let width, height;
for (let entry of entries) {
width = entry.borderBoxSize[0].inlineSize;
height = entry.borderBoxSize[0].blockSize;
}
return [width, height];
}
var module_default6 = src_default6;
// ../alpine/packages/anchor/dist/module.esm.js // ../alpine/packages/anchor/dist/module.esm.js
var min = Math.min; var min = Math.min;
var max = Math.max; var max = Math.max;
@ -7096,7 +7144,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
platform: platformWithCache platform: platformWithCache
}); });
}; };
function src_default6(Alpine3) { function src_default7(Alpine3) {
Alpine3.magic("anchor", (el) => { Alpine3.magic("anchor", (el) => {
if (!el._x_anchor) if (!el._x_anchor)
throw "Alpine: No x-anchor directive found on element using $anchor..."; throw "Alpine: No x-anchor directive found on element using $anchor...";
@ -7154,7 +7202,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let unstyled = modifiers.includes("no-style"); let unstyled = modifiers.includes("no-style");
return { placement, offsetValue, unstyled }; return { placement, offsetValue, unstyled };
} }
var module_default6 = src_default6; var module_default7 = src_default7;
// js/plugins/navigate/history.js // js/plugins/navigate/history.js
var Snapshot = class { var Snapshot = class {
@ -7305,7 +7353,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return createUrlObjectFromString(linkEl.getAttribute("href")); return createUrlObjectFromString(linkEl.getAttribute("href"));
} }
function createUrlObjectFromString(urlString) { function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI); return urlString !== null && new URL(urlString, document.baseURI);
} }
function getUriStringFromUrlObject(urlObject) { function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash; return urlObject.pathname + urlObject.search + urlObject.hash;
@ -7425,10 +7473,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el.removeAttribute("data-scroll-y"); el.removeAttribute("data-scroll-y");
} }
}; };
queueMicrotask(() => {
queueMicrotask(() => { queueMicrotask(() => {
scroll(document.body); scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll); document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
}); });
});
} }
// js/plugins/navigate/persist.js // js/plugins/navigate/persist.js
@ -7730,12 +7780,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let shouldPrefetchOnHover = modifiers.includes("hover"); let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => { shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el); let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => { prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination); storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
}); });
}); });
whenThisLinkIsPressed(el, (whenItIsReleased) => { whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el); let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => { prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination); storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
}); });
@ -8158,6 +8212,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let toAttributes = Array.from(to.attributes); let toAttributes = Array.from(to.attributes);
for (let i = domAttributes.length - 1; i >= 0; i--) { for (let i = domAttributes.length - 1; i >= 0; i--) {
let name = domAttributes[i].name; let name = domAttributes[i].name;
if (name === "style")
continue;
if (!to.hasAttribute(name)) { if (!to.hasAttribute(name)) {
from2.removeAttribute(name); from2.removeAttribute(name);
} }
@ -8165,6 +8221,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
for (let i = toAttributes.length - 1; i >= 0; i--) { for (let i = toAttributes.length - 1; i >= 0; i--) {
let name = toAttributes[i].name; let name = toAttributes[i].name;
let value = toAttributes[i].value; let value = toAttributes[i].value;
if (name === "style")
continue;
if (from2.getAttribute(name) !== value) { if (from2.getAttribute(name) !== value) {
from2.setAttribute(name, value); from2.setAttribute(name, value);
} }
@ -8413,13 +8471,13 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
to.setAttribute("id", fromId); to.setAttribute("id", fromId);
to.id = fromId; to.id = fromId;
} }
function src_default7(Alpine3) { function src_default8(Alpine3) {
Alpine3.morph = morph; Alpine3.morph = morph;
} }
var module_default7 = src_default7; var module_default8 = src_default8;
// ../alpine/packages/mask/dist/module.esm.js // ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.esm.js
function src_default8(Alpine3) { function src_default9(Alpine3) {
Alpine3.directive("mask", (el, { value, expression }, { effect: effect3, evaluateLater: evaluateLater2, cleanup: cleanup2 }) => { Alpine3.directive("mask", (el, { value, expression }, { effect: effect3, evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let templateFn = () => expression; let templateFn = () => expression;
let lastInputValue = ""; let lastInputValue = "";
@ -8581,22 +8639,23 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}); });
return template; return template;
} }
var module_default8 = src_default8; var module_default9 = src_default9;
// js/lifecycle.js // js/lifecycle.js
function start2() { function start2() {
setTimeout(() => ensureLivewireScriptIsntMisplaced()); setTimeout(() => ensureLivewireScriptIsntMisplaced());
dispatch(document, "livewire:init"); dispatch(document, "livewire:init");
dispatch(document, "livewire:initializing"); dispatch(document, "livewire:initializing");
module_default.plugin(module_default7); module_default.plugin(module_default8);
module_default.plugin(history2); module_default.plugin(history2);
module_default.plugin(module_default5); module_default.plugin(module_default5);
module_default.plugin(module_default2);
module_default.plugin(module_default6); module_default.plugin(module_default6);
module_default.plugin(module_default2);
module_default.plugin(module_default7);
module_default.plugin(module_default3); module_default.plugin(module_default3);
module_default.plugin(module_default4); module_default.plugin(module_default4);
module_default.plugin(navigate_default); module_default.plugin(navigate_default);
module_default.plugin(module_default8); module_default.plugin(module_default9);
module_default.addRootSelector(() => "[wire\\:id]"); module_default.addRootSelector(() => "[wire\\:id]");
module_default.onAttributesAdded((el, attributes) => { module_default.onAttributesAdded((el, attributes) => {
if (!Array.from(attributes).some((attribute) => matchesForLivewireDirective(attribute.name))) if (!Array.from(attributes).some((attribute) => matchesForLivewireDirective(attribute.name)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
{"/livewire.js":"87e1046f"} {"/livewire.js":"923613aa"}

View file

@ -621,31 +621,30 @@ h4 {
//border-left: 1px solid #dddddd; //border-left: 1px solid #dddddd;
//border-right: 1px solid #dddddd; //border-right: 1px solid #dddddd;
display: table; display: table;
} }
.row-striped .row:nth-of-type(odd) div { .row-striped .row:nth-of-type(odd) div {
background-color: #f9f9f9; background-color: #f9f9f9;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-striped .row:nth-of-type(even) div { .row-striped .row:nth-of-type(even) div {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-cell; display: table-cell;
word-wrap: break-word;
} }
.row-new-striped { .row-new-striped {
vertical-align: top; vertical-align: top;
line-height: 2.6; padding: 3px;
padding: 0px;
margin-left: 20px;
display: table; display: table;
width: 100%; width: 100%;
padding-right: 20px; word-wrap: break-word;
table-layout:fixed;
} }
/** /**
@ -656,25 +655,28 @@ h4 {
.row-new-striped > .row:nth-of-type(even) { .row-new-striped > .row:nth-of-type(even) {
background: #FFFFFF; background: #FFFFFF;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row; display: table-row;
} }
.row-new-striped > .row:nth-of-type(odd) { .row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8; background-color: #F8F8F8;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
display: table-row; display: table-row;
line-height: 1.9;
padding: 2px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }
.row-new-striped div { .row-new-striped div {
display: table-cell; display: table-cell;
border-top: 1px solid #dddddd; border-top: 1px solid #dddddd;
padding: 6px;
} }

View file

@ -387,5 +387,6 @@ return [
'restore_default_avatar_help' => '', 'restore_default_avatar_help' => '',
'due_checkin_days' => 'Due For Checkin Warning', 'due_checkin_days' => 'Due For Checkin Warning',
'due_checkin_days_help' => 'How many days before the expected checkin of an asset should it be listed in the "Due for checkin" page?', 'due_checkin_days_help' => 'How many days before the expected checkin of an asset should it be listed in the "Due for checkin" page?',
'no_groups' => 'No groups have been created yet. Visit <code>Admin Settings > Permission Groups</code> to add one.',
]; ];

View file

@ -173,6 +173,7 @@ return [
'ulid' => 'The :attribute field must be a valid ULID.', 'ulid' => 'The :attribute field must be a valid ULID.',
'uuid' => 'The :attribute field must be a valid UUID.', 'uuid' => 'The :attribute field must be a valid UUID.',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Custom Validation Language Lines | Custom Validation Language Lines
@ -194,7 +195,7 @@ return [
'custom_field_not_found_on_model' => 'This field seems to exist, but is not available on this Asset Model\'s fieldset.', 'custom_field_not_found_on_model' => 'This field seems to exist, but is not available on this Asset Model\'s fieldset.',
// date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :( // date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :(
// We use this because the default error message for date_format is reflects php Y-m-d, which non-PHP // We use this because the default error message for date_format reflects php Y-m-d, which non-PHP
// people won't know how to format. // people won't know how to format.
'purchase_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', 'purchase_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
'last_audit_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD hh:mm:ss format', 'last_audit_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD hh:mm:ss format',
@ -206,6 +207,13 @@ return [
'checkboxes' => ':attribute contains invalid options.', 'checkboxes' => ':attribute contains invalid options.',
'radio_buttons' => ':attribute is invalid.', 'radio_buttons' => ':attribute is invalid.',
'invalid_value_in_field' => 'Invalid value included in this field', 'invalid_value_in_field' => 'Invalid value included in this field',
'ldap_username_field' => [
'not_in' => '<code>sAMAccountName</code> (mixed case) will likely not work. You should use <code>samaccountname</code> (lowercase) instead.'
],
'ldap_auth_filter_query' => ['not_in' => '<code>uid=samaccountname</code> is probably not a valid auth filter. You probably want <code>uid=</code> '],
'ldap_filter' => ['regex' => 'This value should probably not be wrapped in parentheses.'],
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -301,7 +301,7 @@
{{ trans('general.notes') }} {{ trans('general.notes') }}
</strong> </strong>
</div> </div>
<div class="col-md-9"> <div class="col-md-9" style="word-wrap: break-word;">
{!! nl2br(Helper::parseEscapedMarkedownInline($accessory->notes)) !!} {!! nl2br(Helper::parseEscapedMarkedownInline($accessory->notes)) !!}
</div> </div>
</div> </div>

View file

@ -103,20 +103,23 @@
</div> </div>
@can('self.profile') @can('self.profile')
<div class="col-md-12"> <div class="col-md-12">
<a href="{{ route('profile') }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print"> <a href="{{ route('profile') }}" style="width: 100%;" class="btn btn-sm btn-warning btn-social btn-block hidden-print">
<x-icon type="edit" />
{{ trans('general.editprofile') }} {{ trans('general.editprofile') }}
</a> </a>
</div> </div>
@endcan @endcan
<div class="col-md-12" style="padding-top: 5px;"> <div class="col-md-12" style="padding-top: 5px;">
<a href="{{ route('account.password.index') }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print" target="_blank" rel="noopener"> <a href="{{ route('account.password.index') }}" style="width: 100%;" class="btn btn-sm btn-primary btn-social btn-block hidden-print" target="_blank" rel="noopener">
<x-icon type="password" class="fa-fw" />
{{ trans('general.changepassword') }} {{ trans('general.changepassword') }}
</a> </a>
</div> </div>
@can('self.api') @can('self.api')
<div class="col-md-12" style="padding-top: 5px;"> <div class="col-md-12" style="padding-top: 5px;">
<a href="{{ route('user.api') }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print" target="_blank" rel="noopener"> <a href="{{ route('user.api') }}" style="width: 100%;" class="btn btn-sm btn-primary btn-social btn-block hidden-print" target="_blank" rel="noopener">
<x-icon type="api-key" class="fa-fw" />
{{ trans('general.manage_api_keys') }} {{ trans('general.manage_api_keys') }}
</a> </a>
</div> </div>
@ -124,7 +127,8 @@
<div class="col-md-12" style="padding-top: 5px;"> <div class="col-md-12" style="padding-top: 5px;">
<a href="{{ route('profile.print') }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print" target="_blank" rel="noopener"> <a href="{{ route('profile.print') }}" style="width: 100%;" class="btn btn-sm btn-primary btn-social btn-block hidden-print" target="_blank" rel="noopener">
<x-icon type="print" class="fa-fw" />
{{ trans('admin/users/general.print_assigned') }} {{ trans('admin/users/general.print_assigned') }}
</a> </a>
</div> </div>
@ -134,10 +138,16 @@
@if (!empty($user->email)) @if (!empty($user->email))
<form action="{{ route('profile.email_assets') }}" method="POST"> <form action="{{ route('profile.email_assets') }}" method="POST">
{{ csrf_field() }} {{ csrf_field() }}
<button style="width: 100%;" class="btn btn-sm btn-primary hidden-print" rel="noopener">{{ trans('admin/users/general.email_assigned') }}</button> <button style="width: 100%;" class="btn btn-sm btn-primary btn-social btn-block hidden-print" rel="noopener">
<x-icon type="email" class="fa-fw" />
{{ trans('admin/users/general.email_assigned') }}
</button>
</form> </form>
@else @else
<button style="width: 100%;" class="btn btn-sm btn-primary hidden-print" rel="noopener" disabled title="{{ trans('admin/users/message.user_has_no_email') }}">{{ trans('admin/users/general.email_assigned') }}</button> <button style="width: 100%;" class="btn btn-sm btn-primary btn-social btn-block hidden-print disabled" rel="noopener" disabled title="{{ trans('admin/users/message.user_has_no_email') }}">
<x-icon type="email" class="fa-fw" />
{{ trans('admin/users/general.email_assigned') }}
</button>
@endif @endif
</div> </div>

View file

@ -34,6 +34,8 @@
'required' => true, 'required' => true,
'asset_status_type' => 'RTD', 'asset_status_type' => 'RTD',
'select_id' => 'assigned_assets_select', 'select_id' => 'assigned_assets_select',
'asset_selector_div_id' => 'assets_to_checkout_div',
'asset_ids' => old('selected_assets')
]) ])
@ -42,7 +44,7 @@
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true']) @include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user']) @include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;']) @include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'asset_selector_div_id' => 'assigned_asset', 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;']) @include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;'])
<!-- Checkout/Checkin Date --> <!-- Checkout/Checkin Date -->

View file

@ -189,14 +189,14 @@ dir="{{ Helper::determineLanguageDirection() }}">
action="{{ route('findbytag/hardware') }}" method="get"> action="{{ route('findbytag/hardware') }}" method="get">
<div class="col-xs-12 col-md-12"> <div class="col-xs-12 col-md-12">
<div class="col-xs-12 form-group"> <div class="col-xs-12 form-group">
<label class="sr-only" <label class="sr-only" for="tagSearch">
for="tagSearch">{{ trans('general.lookup_by_tag') }}</label> {{ trans('general.lookup_by_tag') }}
<input type="text" class="form-control" id="tagSearch" name="assetTag" </label>
placeholder="{{ trans('general.lookup_by_tag') }}"> <input type="text" class="form-control" id="tagSearch" name="assetTag" placeholder="{{ trans('general.lookup_by_tag') }}">
<input type="hidden" name="topsearch" value="true" id="search"> <input type="hidden" name="topsearch" value="true" id="search">
</div> </div>
<div class="col-xs-1"> <div class="col-xs-1">
<button type="submit" class="btn btn-primary pull-right"> <button type="submit" id="topSearchButton" class="btn btn-primary pull-right">
<x-icon type="search" /> <x-icon type="search" />
<span class="sr-only">{{ trans('general.search') }}</span> <span class="sr-only">{{ trans('general.search') }}</span>
</button> </button>

View file

@ -25,9 +25,15 @@
<label for="currency" class="col-md-3 control-label"> <label for="currency" class="col-md-3 control-label">
{{ trans('admin/locations/table.currency') }} {{ trans('admin/locations/table.currency') }}
</label> </label>
<div class="col-md-9"> <div class="col-md-7">
{{ Form::text('currency', old('currency', $item->currency), array('class' => 'form-control','placeholder' => 'USD', 'maxlength'=>'3', 'style'=>'width: 60px;', 'aria-label'=>'currency', 'required' => (Helper::checkIfRequired($item, 'currency')) ? true : '')) }} <input class="form-control" style="width:100px" type="text" name="currency" aria-label="currency" id="currency" value="{{ old('currency', $item->currency) }}"{!! (Helper::checkIfRequired($item, 'currency')) ? ' required' : '' !!} maxlength="3" />
{!! $errors->first('currency', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('currency')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
</div> </div>
</div> </div>
@ -40,8 +46,13 @@
{{ trans('admin/locations/table.ldap_ou') }} {{ trans('admin/locations/table.ldap_ou') }}
</label> </label>
<div class="col-md-7"> <div class="col-md-7">
{{ Form::text('ldap_ou', old('ldap_ou', $item->ldap_ou), array('class' => 'form-control', 'required' => (Helper::checkIfRequired($item, 'ldap_ou')) ? true : '')) }} <input class="form-control" type="text" name="ldap_ou" aria-label="ldap_ou" id="ldap_ou" value="{{ old('ldap_ou', $item->ldap_ou) }}"{!! (Helper::checkIfRequired($item, 'ldap_ou')) ? ' required' : '' !!} maxlength="191" />
{!! $errors->first('ldap_ou', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_ou')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
</div> </div>
</div> </div>
@endif @endif

View file

@ -1,5 +1,6 @@
<!-- Asset --> <!-- Asset -->
<div id="assigned_asset" class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}> <div id="{{ $asset_selector_div_id ?? "assigned_asset" }}"
class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
{{ Form::label($fieldname, $translated_name, array('class' => 'col-md-3 control-label')) }} {{ Form::label($fieldname, $translated_name, array('class' => 'col-md-3 control-label')) }}
<div class="col-md-7"> <div class="col-md-7">
<select class="js-data-ajax select2" data-endpoint="hardware" data-placeholder="{{ trans('general.select_asset') }}" aria-label="{{ $fieldname }}" name="{{ $fieldname }}" style="width: 100%" id="{{ (isset($select_id)) ? $select_id : 'assigned_asset_select' }}"{{ (isset($multiple)) ? ' multiple' : '' }}{!! (!empty($asset_status_type)) ? ' data-asset-status-type="' . $asset_status_type . '"' : '' !!}{{ ((isset($required) && ($required =='true'))) ? ' required' : '' }}> <select class="js-data-ajax select2" data-endpoint="hardware" data-placeholder="{{ trans('general.select_asset') }}" aria-label="{{ $fieldname }}" name="{{ $fieldname }}" style="width: 100%" id="{{ (isset($select_id)) ? $select_id : 'assigned_asset_select' }}"{{ (isset($multiple)) ? ' multiple' : '' }}{!! (!empty($asset_status_type)) ? ' data-asset-status-type="' . $asset_status_type . '"' : '' !!}{{ ((isset($required) && ($required =='true'))) ? ' required' : '' }}>
@ -11,6 +12,15 @@
@else @else
@if(!isset($multiple)) @if(!isset($multiple))
<option value="" role="option">{{ trans('general.select_asset') }}</option> <option value="" role="option">{{ trans('general.select_asset') }}</option>
@else
@if(isset($asset_ids))
@foreach($asset_ids as $asset_id)
<option value="{{ $asset_id }}" selected="selected" role="option" aria-selected="true"
role="option">
{{ (\App\Models\Asset::find($asset_id)) ? \App\Models\Asset::find($asset_id)->present()->fullName : '' }}
</option>
@endforeach
@endif
@endif @endif
@endif @endif
</select> </select>

View file

@ -21,7 +21,8 @@
</style> </style>
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form' ]) }} <form method="POST" action="{{ route('settings.alerts.save') }}" autocomplete="off" class="form-horizontal" role="form" id="create-form">
<!-- CSRF Token --> <!-- CSRF Token -->
{{ csrf_field() }} {{ csrf_field() }}
@ -68,12 +69,10 @@
{{ Form::label('alert_email', trans('admin/settings/general.alert_email')) }} {{ Form::label('alert_email', trans('admin/settings/general.alert_email')) }}
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
{{ Form::text('alert_email', old('alert_email', $setting->alert_email), array('class' => 'form-control','placeholder' => 'admin@yourcompany.com')) }} <input type="text" name="alert_email" value="{{ old('alert_email', $setting->alert_email) }}" class="form-control" placeholder="admin@yourcompany.com" maxlength="191">
{!! $errors->first('alert_email', '<span class="alert-msg" aria-hidden="true">:message</span><br>') !!} {!! $errors->first('alert_email', '<span class="alert-msg" aria-hidden="true">:message</span><br>') !!}
<p class="help-block">{{ trans('admin/settings/general.alert_email_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.alert_email_help') }}</p>
</div> </div>
</div> </div>
@ -84,7 +83,7 @@
{{ Form::label('admin_cc_email', trans('admin/settings/general.admin_cc_email')) }} {{ Form::label('admin_cc_email', trans('admin/settings/general.admin_cc_email')) }}
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
{{ Form::text('admin_cc_email', old('admin_cc_email', $setting->admin_cc_email), array('class' => 'form-control','placeholder' => 'admin@yourcompany.com')) }} <input type="text" name="admin_cc_email" value="{{ old('admin_cc_email', $setting->admin_cc_email) }}" class="form-control" placeholder="admin@yourcompany.com" maxlength="191">
{!! $errors->first('admin_cc_email', '<span class="alert-msg" aria-hidden="true">:message</span><br>') !!} {!! $errors->first('admin_cc_email', '<span class="alert-msg" aria-hidden="true">:message</span><br>') !!}
<p class="help-block">{{ trans('admin/settings/general.admin_cc_email_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.admin_cc_email_help') }}</p>
@ -122,7 +121,7 @@
{{ Form::label('audit_interval', trans('admin/settings/general.audit_interval')) }} {{ Form::label('audit_interval', trans('admin/settings/general.audit_interval')) }}
</div> </div>
<div class="input-group col-md-2"> <div class="input-group col-md-2">
{{ Form::text('audit_interval', old('audit_interval', $setting->audit_interval), array('class' => 'form-control','placeholder' => '12', 'maxlength'=>'3', 'style'=>'width: 60px;')) }} {{ Form::text('audit_interval', old('audit_interval', $setting->audit_interval), array('class' => 'form-control','placeholder' => '12', 'maxlength'=>'3')) }}
<span class="input-group-addon">{{ trans('general.months') }}</span> <span class="input-group-addon">{{ trans('general.months') }}</span>
</div> </div>
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
@ -137,7 +136,7 @@
{{ Form::label('audit_warning_days', trans('admin/settings/general.audit_warning_days')) }} {{ Form::label('audit_warning_days', trans('admin/settings/general.audit_warning_days')) }}
</div> </div>
<div class="input-group col-md-2"> <div class="input-group col-md-2">
{{ Form::text('audit_warning_days', old('audit_warning_days', $setting->audit_warning_days), array('class' => 'form-control','placeholder' => '14', 'maxlength'=>'3', 'style'=>'width: 60px;')) }} {{ Form::text('audit_warning_days', old('audit_warning_days', $setting->audit_warning_days), array('class' => 'form-control','placeholder' => '14', 'maxlength'=>'3')) }}
<span class="input-group-addon">{{ trans('general.days') }}</span> <span class="input-group-addon">{{ trans('general.days') }}</span>
</div> </div>
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
@ -152,12 +151,8 @@
{{ Form::label('due_checkin_days', trans('admin/settings/general.due_checkin_days')) }} {{ Form::label('due_checkin_days', trans('admin/settings/general.due_checkin_days')) }}
</div> </div>
<div class="input-group col-md-2"> <div class="input-group col-md-2">
{{ Form::text('due_checkin_days', old('due_checkin_days', $setting->due_checkin_days), array('class' => 'form-control','placeholder' => '14', 'maxlength'=>'3', 'style'=>'width: 60px;')) }} {{ Form::text('due_checkin_days', old('due_checkin_days', $setting->due_checkin_days), array('class' => 'form-control','placeholder' => '14', 'maxlength'=>'3')) }}
<span class="input-group-addon">{{ trans('general.days') }}</span> <span class="input-group-addon">{{ trans('general.days') }}</span>
</div> </div>
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
{!! $errors->first('due_checkin_days', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} {!! $errors->first('due_checkin_days', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}

View file

@ -18,7 +18,7 @@
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form' ]) }} <form method="POST" autocomplete="off" class="form-horizontal" role="form" id="create-form">
<!-- CSRF Token --> <!-- CSRF Token -->
{{csrf_field()}} {{csrf_field()}}

View file

@ -2,7 +2,7 @@
{{-- Page title --}} {{-- Page title --}}
@section('title') @section('title')
Update LDAP/AD Settings {{ trans('admin/settings/general.ldap_ad') }}
@parent @parent
@stop @stop
@ -42,8 +42,7 @@
@endif @endif
<form method="POST" action="{{ route('settings.ldap.save') }}" autocomplete="off" class="form-horizontal" role="form" id="create-form">
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form']) }}
<!-- CSRF Token --> <!-- CSRF Token -->
{{csrf_field()}} {{csrf_field()}}
@ -62,7 +61,7 @@
<h2 class="box-title"> <h2 class="box-title">
<x-icon type="ldap"/> <x-icon type="ldap"/>
{{ trans('admin/settings/general.ldap_ad') }} {{ trans('admin/settings/general.ldap_ad') }}
</h4> </h2>
</div> </div>
<div class="box-body"> <div class="box-body">
@ -76,11 +75,15 @@
<div class="col-md-8"> <div class="col-md-8">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('ldap_enabled', '1', old('ldap_enabled', $setting->ldap_enabled), [((config('app.lock_passwords')===true)) ? 'disabled ': '', 'class' => 'form-control '. $setting->demoMode, $setting->demoMode]) }} {{ Form::checkbox('ldap_enabled', '1', old('ldap_enabled', $setting->ldap_enabled)) }}
{{ trans('admin/settings/general.ldap_enabled') }} {{ trans('admin/settings/general.ldap_enabled') }}
</label> </label>
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -93,13 +96,21 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('is_ad', '1', old('is_ad', $setting->is_ad), [((config('app.lock_passwords')===true)) ? 'disabled ': '', 'class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }} {{ Form::checkbox('is_ad', '1', old('is_ad', $setting->is_ad)) }}
{{ trans('admin/settings/general.is_ad') }} {{ trans('admin/settings/general.is_ad') }}
</label> </label>
{!! $errors->first('is_ad', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('is_ad')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -111,14 +122,23 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('ldap_pw_sync', '1', old('ldap_pw_sync', $setting->ldap_pw_sync), [((config('app.lock_passwords')===true)) ? 'disabled ': '', 'class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }} {{ Form::checkbox('ldap_pw_sync', '1', old('ldap_pw_sync', $setting->ldap_pw_sync)) }}
{{ trans('general.yes') }} {{ trans('general.yes') }}
</label> </label>
<p class="help-block">{{ trans('admin/settings/general.ldap_pw_sync_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.ldap_pw_sync_help') }}</p>
{!! $errors->first('ldap_pw_sync_help', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_pw_sync')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
@ -130,42 +150,43 @@
{{ Form::label('ad_domain', trans('admin/settings/general.ad_domain')) }} {{ Form::label('ad_domain', trans('admin/settings/general.ad_domain')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ad_domain', old('ad_domain', $setting->ad_domain), ['class' => 'form-control','placeholder' => trans('general.example') .'example.com', $setting->demoMode]) }} {{ Form::text('ad_domain', old('ad_domain', $setting->ad_domain), ['class' => 'form-control','placeholder' => trans('general.example') .'example.com']) }}
<p class="help-block">{{ trans('admin/settings/general.ad_domain_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.ad_domain_help') }}</p>
{!! $errors->first('ad_domain', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ad_domain')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div><!-- AD Domain --> </div><!-- AD Domain -->
{{-- NOTICE - this was a feature for AdLdap2-based LDAP syncing, and is already handled in 'classic' LDAP, so we now hide the checkbox (but haven't deleted the field) <!-- AD Append Domain -->
<div class="form-group">
<div class="col-md-3">
{{ Form::label('ad_append_domain', trans('admin/settings/general.ad_append_domain_label')) }}
</div>
<div class="col-md-8">
{{ Form::checkbox('ad_append_domain', '1', old('ad_append_domain', $setting->ad_append_domain),['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }}
{{ trans('admin/settings/general.ad_append_domain') }}
<p class="help-block">{{ trans('admin/settings/general.ad_append_domain_help') }}</p>
{!! $errors->first('ad_append_domain', '<span class="alert-msg">:message</span>') !!}
@if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p>
@endif
</div>
</div> --}}
<!-- LDAP Client-Side TLS key --> <!-- LDAP Client-Side TLS key -->
<div class="form-group {{ $errors->has('ldap_client_tls_key') ? 'error' : '' }}"> <div class="form-group {{ $errors->has('ldap_client_tls_key') ? 'error' : '' }}">
<div class="col-md-3"> <div class="col-md-3">
{{ Form::label('ldap_client_tls_key', trans('admin/settings/general.ldap_client_tls_key')) }} {{ Form::label('ldap_client_tls_key', trans('admin/settings/general.ldap_client_tls_key')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::textarea('ldap_client_tls_key', old('ldap_client_tls_key', $setting->ldap_client_tls_key), ['class' => 'form-control','placeholder' => trans('general.example') .'-----BEGIN RSA PRIVATE KEY-----'."\r\n1234567890\r\n-----END RSA PRIVATE KEY----- {{ Form::textarea('ldap_client_tls_key', old('ldap_client_tls_key', $setting->ldap_client_tls_key), ['class' => 'form-control','placeholder' => trans('general.example') .'-----BEGIN RSA PRIVATE KEY-----'."\r\n1234567890\r\n-----END RSA PRIVATE KEY-----"]) }}
", $setting->demoMode]) }} @error('ldap_client_tls_key')
{!! $errors->first('ldap_client_tls_key', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} <span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div><!-- LDAP Client-Side TLS key --> </div><!-- LDAP Client-Side TLS key -->
@ -176,11 +197,20 @@
{{ Form::label('ldap_client_tls_cert', trans('admin/settings/general.ldap_client_tls_cert')) }} {{ Form::label('ldap_client_tls_cert', trans('admin/settings/general.ldap_client_tls_cert')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::textarea('ldap_client_tls_cert', old('ldap_client_tls_cert', $setting->ldap_client_tls_cert), ['class' => 'form-control','placeholder' => trans('general.example') .'-----BEGIN CERTIFICATE-----'."\r\n1234567890\r\n-----END CERTIFICATE-----", $setting->demoMode]) }} {{ Form::textarea('ldap_client_tls_cert', old('ldap_client_tls_cert', $setting->ldap_client_tls_cert), ['class' => 'form-control','placeholder' => trans('general.example') .'-----BEGIN CERTIFICATE-----'."\r\n1234567890\r\n-----END CERTIFICATE-----"]) }}
<p class="help-block">{{ trans('admin/settings/general.ldap_client_tls_cert_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.ldap_client_tls_cert_help') }}</p>
{!! $errors->first('ldap_client_tls_cert', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_client_tls_cert')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div><!-- LDAP Client-Side TLS certificate --> </div><!-- LDAP Client-Side TLS certificate -->
@ -191,11 +221,21 @@
{{ Form::label('ldap_server', trans('admin/settings/general.ldap_server')) }} {{ Form::label('ldap_server', trans('admin/settings/general.ldap_server')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_server', old('ldap_server', $setting->ldap_server), ['class' => 'form-control','placeholder' => trans('general.example') .'ldap://ldap.example.com', $setting->demoMode]) }} {{ Form::text('ldap_server', old('ldap_server', $setting->ldap_server), ['class' => 'form-control','placeholder' => trans('general.example') .'ldap://ldap.example.com']) }}
@error('ldap_server')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
<p class="help-block">{{ trans('admin/settings/general.ldap_server_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.ldap_server_help') }}</p>
{!! $errors->first('ldap_server', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div><!-- LDAP Server --> </div><!-- LDAP Server -->
@ -207,12 +247,21 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('ldap_tls', '1', old('ldap_tls', $setting->ldap_tls),['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }} {{ Form::checkbox('ldap_tls', '1', old('ldap_tls', $setting->ldap_tls)) }}
{{ trans('admin/settings/general.ldap_tls_help') }} {{ trans('admin/settings/general.ldap_tls_help') }}
</label> </label>
{!! $errors->first('ldap_tls', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_tls')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -224,13 +273,24 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="form-control"> <label class="form-control">
{{ Form::checkbox('ldap_server_cert_ignore', '1', old('ldap_server_cert_ignore', $setting->ldap_server_cert_ignore),['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }} {{ Form::checkbox('ldap_server_cert_ignore', '1', old('ldap_server_cert_ignore', $setting->ldap_server_cert_ignore)) }}
{{ trans('admin/settings/general.ldap_server_cert_ignore') }} {{ trans('admin/settings/general.ldap_server_cert_ignore') }}
</label> </label>
{!! $errors->first('ldap_server_cert_ignore', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_server_cert_ignore')
<p class="help-block">{{ trans('admin/settings/general.ldap_server_cert_help') }}</p> <span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
<p class="help-block">
{{ trans('admin/settings/general.ldap_server_cert_help') }}
</p>
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -241,10 +301,19 @@
{{ Form::label('ldap_uname', trans('admin/settings/general.ldap_uname')) }} {{ Form::label('ldap_uname', trans('admin/settings/general.ldap_uname')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_uname', old('ldap_uname', $setting->ldap_uname), ['class' => 'form-control','autocomplete' => 'off', 'placeholder' => trans('general.example') .'binduser@example.com', $setting->demoMode]) }} {{ Form::text('ldap_uname', old('ldap_uname', $setting->ldap_uname), ['class' => 'form-control','autocomplete' => 'off', 'placeholder' => trans('general.example') .'binduser@example.com']) }}
{!! $errors->first('ldap_uname', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_uname')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -255,10 +324,19 @@
{{ Form::label('ldap_pword', trans('admin/settings/general.ldap_pword')) }} {{ Form::label('ldap_pword', trans('admin/settings/general.ldap_pword')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::password('ldap_pword', ['class' => 'form-control', 'autocomplete' => 'off', 'onfocus' => "this.removeAttribute('readonly');", $setting->demoMode, ' readonly']) }} {{ Form::password('ldap_pword', ['class' => 'form-control', 'autocomplete' => 'off', 'onfocus' => "this.removeAttribute('readonly');", ' readonly']) }}
{!! $errors->first('ldap_pword', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_pword')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -269,10 +347,19 @@
{{ Form::label('ldap_basedn', trans('admin/settings/general.ldap_basedn')) }} {{ Form::label('ldap_basedn', trans('admin/settings/general.ldap_basedn')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_basedn', old('ldap_basedn', $setting->ldap_basedn), ['class' => 'form-control', 'placeholder' => trans('general.example') .'cn=users/authorized,dc=example,dc=com', $setting->demoMode]) }} {{ Form::text('ldap_basedn', old('ldap_basedn', $setting->ldap_basedn), ['class' => 'form-control', 'placeholder' => trans('general.example') .'cn=users/authorized,dc=example,dc=com']) }}
{!! $errors->first('ldap_basedn', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_basedn')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -283,10 +370,19 @@
{{ Form::label('ldap_filter', trans('admin/settings/general.ldap_filter')) }} {{ Form::label('ldap_filter', trans('admin/settings/general.ldap_filter')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_filter', old('ldap_filter', $setting->ldap_filter), ['class' => 'form-control','placeholder' => trans('general.example') .'&(cn=*)', $setting->demoMode]) }} <input type="text" name="ldap_filter" id="ldap_filter" value="{{ old('ldap_filter', $setting->ldap_filter) }}" class="form-control" placeholder="{{ trans('general.example') .'&(cn=*)' }}">
{!! $errors->first('ldap_filter', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_filter')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -297,10 +393,19 @@
{{ Form::label('ldap_username_field', trans('admin/settings/general.ldap_username_field')) }} {{ Form::label('ldap_username_field', trans('admin/settings/general.ldap_username_field')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_username_field', old('ldap_username_field', $setting->ldap_username_field), ['class' => 'form-control','placeholder' => trans('general.example') .'samaccountname', $setting->demoMode]) }} <input type="text" name="ldap_username_field" id="ldap_username_field" value="{{ old('ldap_username_field', $setting->ldap_username_field) }}" class="form-control" placeholder="{{ trans('general.example') .'samaccountname' }}">
{!! $errors->first('ldap_username_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_username_field')
<span class="alert-msg">
<x-icon type="x" />
{!! $message !!}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -311,10 +416,19 @@
{{ Form::label('ldap_lname_field', trans('admin/settings/general.ldap_lname_field')) }} {{ Form::label('ldap_lname_field', trans('admin/settings/general.ldap_lname_field')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_lname_field', old('ldap_lname_field', $setting->ldap_lname_field), ['class' => 'form-control','placeholder' => trans('general.example') .'sn', $setting->demoMode]) }} <input type="text" name="ldap_lname_field" id="ldap_lname_field" value="{{ old('ldap_lname_field', $setting->ldap_lname_field) }}" class="form-control" placeholder="{{ trans('general.example') .'sn' }}">
{!! $errors->first('ldap_lname_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_lname_field')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -325,10 +439,19 @@
{{ Form::label('ldap_fname_field', trans('admin/settings/general.ldap_fname_field')) }} {{ Form::label('ldap_fname_field', trans('admin/settings/general.ldap_fname_field')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_fname_field', old('ldap_fname_field', $setting->ldap_fname_field), ['class' => 'form-control', 'placeholder' => trans('general.example') .'givenname', $setting->demoMode]) }} <input type="text" name="ldap_fname_field" id="ldap_fname_field" value="{{ old('ldap_fname_field', $setting->ldap_fname_field) }}" class="form-control" placeholder="{{ trans('general.example') .'givenname' }}">
{!! $errors->first('ldap_fname_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_fname_field')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -336,13 +459,23 @@
<!-- LDAP Auth Filter Query --> <!-- LDAP Auth Filter Query -->
<div class="form-group {{ $errors->has('ldap_auth_filter_query') ? 'error' : '' }}"> <div class="form-group {{ $errors->has('ldap_auth_filter_query') ? 'error' : '' }}">
<div class="col-md-3"> <div class="col-md-3">
{{ Form::label('ldap_auth_filter_query', trans('admin/settings/general.ldap_auth_filter_query')) }} <label for="ldap_auth_filter_query">{{ trans('admin/settings/general.ldap_auth_filter_query') }}</label>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_auth_filter_query', old('ldap_auth_filter_query', $setting->ldap_auth_filter_query), ['class' => 'form-control','placeholder' => trans('general.example') .'uid=', $setting->demoMode]) }}
{!! $errors->first('ldap_auth_filter_query', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} <input type="text" name="ldap_auth_filter_query" id="ldap_auth_filter_query" value="{{ old('ldap_auth_filter_query', $setting->ldap_auth_filter_query) }}" class="form-control" placeholder="{{ trans('general.example') .'uid=' }}">
@error('ldap_auth_filter_query')
<span class="alert-msg">
<x-icon type="x" />
{!! $message !!}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -364,7 +497,6 @@
@endforeach @endforeach
</ul> </ul>
<span class="help-block">{{ trans('admin/users/general.group_memberships_helpblock') }}</span> <span class="help-block">{{ trans('admin/users/general.group_memberships_helpblock') }}</span>
@else @else
<div class="controls"> <div class="controls">
@ -383,7 +515,7 @@
</div> </div>
@endif @endif
@else @else
<p>No groups have been created yet. Visit <code>Admin Settings > Permission Groups</code> to add one.</p> <p>{!! trans('admin/settings/general.no_groups') !!}</p>
@endif @endif
</div> </div>
@ -395,13 +527,21 @@
{{ Form::label('ldap_active_flag', trans('admin/settings/general.ldap_active_flag')) }} {{ Form::label('ldap_active_flag', trans('admin/settings/general.ldap_active_flag')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_active_flag', old('ldap_active_flag', $setting->ldap_active_flag), ['class' => 'form-control', $setting->demoMode]) }} <input type="text" name="ldap_active_flag" id="ldap_active_flag" value="{{ old('ldap_active_flag', $setting->ldap_active_flag) }}" class="form-control">
<p class="help-block">{!! trans('admin/settings/general.ldap_activated_flag_help') !!}</p> <p class="help-block">{!! trans('admin/settings/general.ldap_activated_flag_help') !!}</p>
{!! $errors->first('ldap_active_flag', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_active_flag')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -412,10 +552,19 @@
{{ Form::label('ldap_emp_num', trans('admin/settings/general.ldap_emp_num')) }} {{ Form::label('ldap_emp_num', trans('admin/settings/general.ldap_emp_num')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_emp_num', old('ldap_emp_num', $setting->ldap_emp_num), ['class' => 'form-control','placeholder' => trans('general.example') .'employeenumber/employeeid', $setting->demoMode]) }} {{ Form::text('ldap_emp_num', old('ldap_emp_num', $setting->ldap_emp_num), ['class' => 'form-control','placeholder' => trans('general.example') .'employeenumber/employeeid']) }}
{!! $errors->first('ldap_emp_num', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_emp_num')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -425,10 +574,20 @@
{{ Form::label('ldap_dept', trans('admin/settings/general.ldap_dept')) }} {{ Form::label('ldap_dept', trans('admin/settings/general.ldap_dept')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_dept', old('ldap_dept', $setting->ldap_dept), ['class' => 'form-control','placeholder' => trans('general.example') .'department', $setting->demoMode]) }} {{ Form::text('ldap_dept', old('ldap_dept', $setting->ldap_dept), ['class' => 'form-control','placeholder' => trans('general.example') .'department']) }}
{!! $errors->first('ldap_dept', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@error('ldap_dept')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -438,10 +597,19 @@
{{ Form::label('ldap_dept', trans('admin/settings/general.ldap_manager')) }} {{ Form::label('ldap_dept', trans('admin/settings/general.ldap_manager')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_manager', old('ldap_manager', $setting->ldap_manager), ['class' => 'form-control','placeholder' => trans('general.example') .'manager', $setting->demoMode]) }} {{ Form::text('ldap_manager', old('ldap_manager', $setting->ldap_manager), ['class' => 'form-control','placeholder' => trans('general.example') .'manager']) }}
{!! $errors->first('ldap_manager', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_manager')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -452,10 +620,19 @@
{{ Form::label('ldap_email', trans('admin/settings/general.ldap_email')) }} {{ Form::label('ldap_email', trans('admin/settings/general.ldap_email')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_email', old('ldap_email', $setting->ldap_email), ['class' => 'form-control','placeholder' => trans('general.example') .'mail', $setting->demoMode]) }} {{ Form::text('ldap_email', old('ldap_email', $setting->ldap_email), ['class' => 'form-control','placeholder' => trans('general.example') .'mail']) }}
{!! $errors->first('ldap_email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_email')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -466,10 +643,19 @@
{{ Form::label('ldap_phone', trans('admin/settings/general.ldap_phone')) }} {{ Form::label('ldap_phone', trans('admin/settings/general.ldap_phone')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_phone', old('ldap_phone', $setting->ldap_phone_field), ['class' => 'form-control','placeholder' => trans('general.example') .'telephonenumber', $setting->demoMode]) }} {{ Form::text('ldap_phone', old('ldap_phone', $setting->ldap_phone_field), ['class' => 'form-control','placeholder' => trans('general.example') .'telephonenumber']) }}
{!! $errors->first('ldap_phone', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_phone')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -480,10 +666,19 @@
{{ Form::label('ldap_jobtitle', trans('admin/settings/general.ldap_jobtitle')) }} {{ Form::label('ldap_jobtitle', trans('admin/settings/general.ldap_jobtitle')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_jobtitle', old('ldap_jobtitle', $setting->ldap_jobtitle), ['class' => 'form-control','placeholder' => trans('general.example') .'title', $setting->demoMode]) }} {{ Form::text('ldap_jobtitle', old('ldap_jobtitle', $setting->ldap_jobtitle), ['class' => 'form-control','placeholder' => trans('general.example') .'title']) }}
{!! $errors->first('ldap_jobtitle', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_jobtitle')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -494,10 +689,19 @@
{{ Form::label('ldap_country', trans('admin/settings/general.ldap_country')) }} {{ Form::label('ldap_country', trans('admin/settings/general.ldap_country')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_country', old('ldap_country', $setting->ldap_country), ['class' => 'form-control','placeholder' => trans('general.example') .'c', $setting->demoMode]) }} {{ Form::text('ldap_country', old('ldap_country', $setting->ldap_country), ['class' => 'form-control','placeholder' => trans('general.example') .'c']) }}
{!! $errors->first('ldap_country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_country')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -507,11 +711,20 @@
{{ Form::label('ldap_location', trans('admin/settings/general.ldap_location')) }} {{ Form::label('ldap_location', trans('admin/settings/general.ldap_location')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('ldap_location', old('ldap_location', $setting->ldap_location), ['class' => 'form-control','placeholder' => trans('general.example') .'physicaldeliveryofficename', $setting->demoMode]) }} {{ Form::text('ldap_location', old('ldap_location', $setting->ldap_location), ['class' => 'form-control','placeholder' => trans('general.example') .'physicaldeliveryofficename']) }}
<p class="help-block">{!! trans('admin/settings/general.ldap_location_help') !!}</p> <p class="help-block">{!! trans('admin/settings/general.ldap_location_help') !!}</p>
{!! $errors->first('ldap_location', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('ldap_location')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div> </div>
@ -523,7 +736,7 @@
{{ Form::label('test_ldap_sync', 'Test LDAP Sync') }} {{ Form::label('test_ldap_sync', 'Test LDAP Sync') }}
</div> </div>
<div class="col-md-8" id="ldaptestrow"> <div class="col-md-8" id="ldaptestrow">
<a {{ $setting->demoMode }} class="btn btn-default btn-sm" id="ldaptest" style="margin-right: 10px;">{{ trans('admin/settings/general.ldap_test_sync') }}</a> <a class="btn btn-default btn-sm" id="ldaptest" style="margin-right: 10px;">{{ trans('admin/settings/general.ldap_test_sync') }}</a>
</div> </div>
<div class="col-md-8 col-md-offset-3"> <div class="col-md-8 col-md-offset-3">
<br /> <br />
@ -532,7 +745,10 @@
<div class="col-md-8 col-md-offset-3"> <div class="col-md-8 col-md-offset-3">
<p class="help-block">{{ trans('admin/settings/general.ldap_login_sync_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.ldap_login_sync_help') }}</p>
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
@ -578,11 +794,20 @@
{{ Form::label('custom_forgot_pass_url', trans('admin/settings/general.custom_forgot_pass_url')) }} {{ Form::label('custom_forgot_pass_url', trans('admin/settings/general.custom_forgot_pass_url')) }}
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{{ Form::text('custom_forgot_pass_url', old('custom_forgot_pass_url', $setting->custom_forgot_pass_url), ['class' => 'form-control','placeholder' => trans('general.example') .'https://my.ldapserver-forgotpass.com', $setting->demoMode]) }} {{ Form::text('custom_forgot_pass_url', old('custom_forgot_pass_url', $setting->custom_forgot_pass_url), ['class' => 'form-control','placeholder' => trans('general.example') .'https://my.ldapserver-forgotpass.com']) }}
<p class="help-block">{{ trans('admin/settings/general.custom_forgot_pass_url_help') }}</p> <p class="help-block">{{ trans('admin/settings/general.custom_forgot_pass_url_help') }}</p>
{!! $errors->first('custom_forgot_pass_url', '<span class="alert-msg" aria-hidden="true">:message</span>') !!} @error('custom_forgot_pass_url')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@if (config('app.lock_passwords')===true) @if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p> <p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif @endif
</div> </div>
</div><!-- LDAP Server --> </div><!-- LDAP Server -->
@ -607,9 +832,10 @@
{{Form::close()}} {{Form::close()}}
@stop @endsection
@push('js') @push('js')
<script nonce="{{ csrf_token() }}"> <script nonce="{{ csrf_token() }}">
@ -618,11 +844,58 @@
* Check to see if is_ad is checked, if not disable the ad_domain field * Check to see if is_ad is checked, if not disable the ad_domain field
*/ */
$(function() { $(function() {
// If the app is locked, disable all fields except the top search fields
@if (config('app.lock_passwords') === true)
$("input").prop('disabled', 'disabled');
$("textarea").prop('disabled', 'disabled');
$("button").prop('disabled', 'disabled');
$("#tagSearch").removeAttr("disabled");
$("#search").removeAttr("disabled");
$("#topSearchButton").removeAttr("disabled");
@endif
if ($('#is_ad').prop('checked') === false) { if ($('#is_ad').prop('checked') === false) {
$('#ad_domain').prop('disabled', 'disabled'); $('#ad_domain').prop('disabled', 'disabled');
} else { $("#ad_domain").prop('required',false);
//$('#ldap_server').prop('disabled', 'disabled');
} }
// Mark fields as required if LDAP is enabled
if ($('#ldap_enabled').prop('checked') === false) {
$("#ldap_server").prop('required',false);
$("#ldap_auth_filter_query").prop('required',false);
$("#ldap_filter").prop('required',false);
$("#ldap_username_field").prop('required',false);
$("#ldap_uname").prop('required',false);
$("#ldap_pword").prop('required',false);
$("#ldap_basedn").prop('required',false);
$("#ldap_fname_field").prop('required',false);
}
$("#ldap_enabled").change(function() {
if (this.checked) {
$("#ldap_server").prop('required',true);
$("#ldap_auth_filter_query").prop('required',true);
$("#ldap_filter").prop('required',true);
$("#ldap_uname").prop('required',true);
$("#ldap_username_field").prop('required',true);
$("#ldap_pword").prop('required',true);
$("#ldap_basedn").prop('required',true);
} else {
$("#ldap_server").prop('required',false);
$("#ldap_auth_filter_query").prop('required',false);
$("#ldap_filter").prop('required',false);
$("#ldap_username_field").prop('required',false);
$("#ldap_pword").prop('required',false);
$("#ldap_basedn").prop('required',false);
$("#ldap_fname_field").prop('required',false);
}
});
}); });
$("#is_ad").change(function() { $("#is_ad").change(function() {
@ -649,7 +922,7 @@
$("#ldaptest").click(function () { $("#ldaptest").click(function () {
$("#ldapad_test_results").removeClass('hidden text-success text-danger'); $("#ldapad_test_results").removeClass('hidden text-success text-danger');
$("#ldapad_test_results").html(''); $("#ldapad_test_results").html('');
$("#ldapad_test_results").html('<i class="fas fa-spinner spin"></i> {{ trans('admin/settings/message.ldap.testing') }}'); $("#ldapad_test_results").html('<x-icon type="spinner" /> {{ trans('admin/settings/message.ldap.testing') }}');
$.ajax({ $.ajax({
url: '{{ route('api.settings.ldaptest') }}', url: '{{ route('api.settings.ldaptest') }}',
type: 'GET', type: 'GET',
@ -698,8 +971,8 @@
*/ */
function buildLdapTestResults(results) { function buildLdapTestResults(results) {
let html = '<ul style="list-style: none;padding-left: 5px;">' let html = '<ul style="list-style: none;padding-left: 5px;">'
html += '<li class="text-success"><i class="fas fa-check" aria-hidden="true"></i> ' + results.login.message + ' </li>' html += '<li class="text-success"><i class="fas fa-check""></i> ' + results.login.message + ' </li>'
html += '<li class="text-success"><i class="fas fa-check" aria-hidden="true"></i> ' + results.bind.message + ' </li>' html += '<li class="text-success"><i class="fas fa-check""></i> ' + results.bind.message + ' </li>'
html += '</ul>' html += '</ul>'
html += '<div style="overflow:auto;">' html += '<div style="overflow:auto;">'
html += '<div>{{ trans('admin/settings/message.ldap.sync_success') }}</div>' html += '<div>{{ trans('admin/settings/message.ldap.sync_success') }}</div>'
@ -738,12 +1011,13 @@
return body; return body;
} }
$("#ldaptestlogin").click(function(){ $("#ldaptestlogin").click(function(){
$("#ldaptestloginrow").removeClass('text-success'); $("#ldaptestloginrow").removeClass('text-success');
$("#ldaptestloginrow").removeClass('text-danger'); $("#ldaptestloginrow").removeClass('text-danger');
$("#ldaptestloginstatus").removeClass('text-danger'); $("#ldaptestloginstatus").removeClass('text-danger');
$("#ldaptestloginstatus").html(''); $("#ldaptestloginstatus").html('');
$("#ldaptestloginicon").html('<i class="fas fa-spinner spin"></i> {{ trans('admin/settings/message.ldap.testing_authentication') }}'); $("#ldaptestloginicon").html('<x-icon type="spinner" /> {{ trans('admin/settings/message.ldap.testing_authentication') }}');
$.ajax({ $.ajax({
url: '{{ route('api.settings.ldaptestlogin') }}', url: '{{ route('api.settings.ldaptestlogin') }}',
type: 'POST', type: 'POST',
@ -803,9 +1077,6 @@
} }
} }
}); });
}); });
</script> </script>

View file

@ -16,7 +16,8 @@
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form' ]) }} <form method="POST" autocomplete="off" class="form-horizontal" role="form" id="create-form">
<!-- CSRF Token --> <!-- CSRF Token -->
{{ csrf_field() }} {{ csrf_field() }}

View file

@ -2,7 +2,7 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
@if (count($users) === 1) @if ((isset($users) && count($users) === 1))
<title>{{ trans('general.assigned_to', ['name' => $users[0]->present()->fullName()]) }} - {{ date('Y-m-d H:i', time()) }}</title> <title>{{ trans('general.assigned_to', ['name' => $users[0]->present()->fullName()]) }} - {{ date('Y-m-d H:i', time()) }}</title>
@else @else
<title>{{ trans('admin/users/general.print_assigned') }} - {{ date('Y-m-d H:i', time()) }}</title> <title>{{ trans('admin/users/general.print_assigned') }} - {{ date('Y-m-d H:i', time()) }}</title>
@ -37,10 +37,6 @@
size: A4; size: A4;
} }
#start_of_user_section {
break-before: page;
}
.print-logo { .print-logo {
max-height: 40px; max-height: 40px;
} }
@ -51,13 +47,6 @@
} }
</style> </style>
<script nonce="{{ csrf_token() }}">
window.snipeit = {
settings: {
"per_page": 50
}
};
</script>
</head> </head>
<body> <body>
@ -384,8 +373,11 @@
</table> </table>
@endif @endif
@php
if (!empty($eulas)) $eulas = array_unique($eulas);
@endphp
{{-- This may have been render at the top of the page if we're rendering more than one user... --}} {{-- This may have been render at the top of the page if we're rendering more than one user... --}}
@if (count($users) === 1) @if (count($users) === 1 && !empty($eulas))
<p></p> <p></p>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-default hidden-print" type="button" data-toggle="collapse" data-target=".eula-row" aria-expanded="false" aria-controls="eula-row" title="EULAs"> <button class="btn btn-default hidden-print" type="button" data-toggle="collapse" data-target=".eula-row" aria-expanded="false" aria-controls="eula-row" title="EULAs">
@ -395,19 +387,16 @@
@endif @endif
<table style="margin-top: 80px;"> <table style="margin-top: 80px;">
@if (!empty($eulas))
<tr class="collapse eula-row"> <tr class="collapse eula-row">
<td style="padding-right: 10px; vertical-align: top; font-weight: bold;">EULA</td> <td style="padding-right: 10px; vertical-align: top; font-weight: bold;">EULA</td>
<td style="padding-right: 10px; vertical-align: top; padding-bottom: 80px;" colspan="3"> <td style="padding-right: 10px; vertical-align: top; padding-bottom: 80px;" colspan="3">
@php
if (!empty($eulas)) $eulas = array_unique($eulas);
@endphp
@if (!empty($eulas))
@foreach ($eulas as $key => $eula) @foreach ($eulas as $key => $eula)
{!! $eula !!} {!! $eula !!}
@endforeach @endforeach
@endif
</td> </td>
</tr> </tr>
@endif
<tr> <tr>
<td style="padding-right: 10px; vertical-align: top; font-weight: bold;">{{ trans('general.signed_off_by') }}:</td> <td style="padding-right: 10px; vertical-align: top; font-weight: bold;">{{ trans('general.signed_off_by') }}:</td>
<td style="padding-right: 10px; vertical-align: top;">______________________________________</td> <td style="padding-right: 10px; vertical-align: top;">______________________________________</td>

View file

@ -31,7 +31,7 @@
<x-icon type="assets" class="fa-2x" /> <x-icon type="assets" class="fa-2x" />
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.assets') }} <span class="hidden-xs hidden-sm">{{ trans('general.assets') }}
{!! ($user->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($user->assets()->AssetsForShow()->count()).'</badge>' : '' !!} {!! ($user->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($user->assets()->AssetsForShow()->withoutTrashed()->count()).'</badge>' : '' !!}
</span> </span>
</a> </a>
</li> </li>
@ -178,8 +178,6 @@
<img src="{{ $user->present()->gravatar() }}" class=" img-thumbnail hidden-print" style="margin-bottom: 20px;" alt="{{ $user->present()->fullName() }}"> <img src="{{ $user->present()->gravatar() }}" class=" img-thumbnail hidden-print" style="margin-bottom: 20px;" alt="{{ $user->present()->fullName() }}">
</div> </div>
@can('update', $user) @can('update', $user)
<div class="col-md-12"> <div class="col-md-12">
<a href="{{ route('users.edit', $user->id) }}" style="width: 100%;" class="btn btn-sm btn-warning btn-social hidden-print"> <a href="{{ route('users.edit', $user->id) }}" style="width: 100%;" class="btn btn-sm btn-warning btn-social hidden-print">

View file

@ -536,13 +536,16 @@ Route::group(['middleware' => 'web'], function () {
)->name('logout.post'); )->name('logout.post');
}); });
//Auth::routes();
Route::get( /**
* Health check route - skip middleware
*/
Route::withoutMiddleware(['web'])->get(
'/health', '/health',
[HealthController::class, 'get'] [HealthController::class, 'get']
)->name('health'); )->name('health');
Route::middleware(['auth'])->get( Route::middleware(['auth'])->get(
'/', '/',
[DashboardController::class, 'index'] [DashboardController::class, 'index']

View file

@ -0,0 +1,59 @@
<?php
namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\TestCase;
class AccessoriesForSelectListTest extends TestCase implements TestsFullMultipleCompaniesSupport
{
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$accessoryA = Accessory::factory()->for($companyA)->create();
$accessoryB = Accessory::factory()->for($companyB)->create();
$superuser = User::factory()->superuser()->create();
$userInCompanyA = $companyA->users()->save(User::factory()->viewAccessories()->make());
$userInCompanyB = $companyB->users()->save(User::factory()->viewAccessories()->make());
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userInCompanyA)
->getJson(route('api.accessories.selectlist'))
->assertOk()
->assertJsonPath('total_count', 1)
->assertResponseContainsInResults($accessoryA)
->assertResponseDoesNotContainInResults($accessoryB);
$this->actingAsForApi($userInCompanyB)
->getJson(route('api.accessories.selectlist'))
->assertOk()
->assertJsonPath('total_count', 1)
->assertResponseDoesNotContainInResults($accessoryA)
->assertResponseContainsInResults($accessoryB);
$this->actingAsForApi($superuser)
->getJson(route('api.accessories.selectlist'))
->assertOk()
->assertJsonPath('total_count', 2)
->assertResponseContainsInResults($accessoryA)
->assertResponseContainsInResults($accessoryB);
}
public function testCanGetAccessoriesForSelectList()
{
[$accessoryA, $accessoryB] = Accessory::factory()->count(2)->create();
$this->actingAsForApi(User::factory()->viewAccessories()->create())
->getJson(route('api.accessories.selectlist'))
->assertOk()
->assertJsonPath('total_count', 2)
->assertResponseContainsInResults($accessoryA)
->assertResponseContainsInResults($accessoryB);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class IndexAccessoryCheckoutsTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$accessory = Accessory::factory()->create();
$this->actingAsForApi(User::factory()->create())
->getJson(route('api.accessories.checkedout', $accessory))
->assertForbidden();
}
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$accessoryA = Accessory::factory()->for($companyA)->create();
$accessoryB = Accessory::factory()->for($companyB)->create();
$superuser = User::factory()->superuser()->create();
$userInCompanyA = $companyA->users()->save(User::factory()->viewAccessories()->make());
$userInCompanyB = $companyB->users()->save(User::factory()->viewAccessories()->make());
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userInCompanyA)
->getJson(route('api.accessories.checkedout', $accessoryB))
->assertStatusMessageIs('error');
$this->actingAsForApi($userInCompanyB)
->getJson(route('api.accessories.checkedout', $accessoryA))
->assertStatusMessageIs('error');
$this->actingAsForApi($superuser)
->getJson(route('api.accessories.checkedout', $accessoryA))
->assertOk();
}
public function testCanGetAccessoryCheckouts()
{
[$userA, $userB] = User::factory()->count(2)->create();
$accessory = Accessory::factory()->checkedOutToUsers([$userA, $userB])->create();
$this->assertEquals(2, $accessory->checkouts()->count());
$this->actingAsForApi(User::factory()->viewAccessories()->create())
->getJson(route('api.accessories.checkedout', $accessory))
->assertOk()
->assertJsonPath('total', 2)
->assertJsonPath('rows.0.assigned_to.id', $userA->id)
->assertJsonPath('rows.1.assigned_to.id', $userB->id);
}
public function testCanGetAccessoryCheckoutsWithOffsetAndLimitInQueryString()
{
[$userA, $userB, $userC] = User::factory()->count(3)->create();
$accessory = Accessory::factory()->checkedOutToUsers([$userA, $userB, $userC])->create();
$actor = $this->actingAsForApi(User::factory()->viewAccessories()->create());
$actor->getJson(route('api.accessories.checkedout', ['accessory' => $accessory->id, 'limit' => 1]))
->assertOk()
->assertJsonPath('total', 3)
->assertJsonPath('rows.0.assigned_to.id', $userA->id);
$actor->getJson(route('api.accessories.checkedout', ['accessory' => $accessory->id, 'limit' => 2, 'offset' => 1]))
->assertOk()
->assertJsonPath('total', 3)
->assertJsonPath('rows.0.assigned_to.id', $userB->id)
->assertJsonPath('rows.1.assigned_to.id', $userC->id);
}
}

View file

@ -2,15 +2,69 @@
namespace Tests\Feature\Accessories\Api; namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User; use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase; use Tests\TestCase;
class IndexAccessoryTest extends TestCase class IndexAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{ {
public function testPermissionRequiredToViewAccessoriesIndex() public function testRequiresPermission()
{ {
$this->actingAsForApi(User::factory()->create()) $this->actingAsForApi(User::factory()->create())
->getJson(route('api.accessories.index')) ->getJson(route('api.accessories.index'))
->assertForbidden(); ->assertForbidden();
} }
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$accessoryA = Accessory::factory()->for($companyA)->create(['name' => 'Accessory A']);
$accessoryB = Accessory::factory()->for($companyB)->create(['name' => 'Accessory B']);
$accessoryC = Accessory::factory()->for($companyB)->create(['name' => 'Accessory C']);
$superUser = $companyA->users()->save(User::factory()->superuser()->make());
$userInCompanyA = $companyA->users()->save(User::factory()->viewAccessories()->make());
$userInCompanyB = $companyB->users()->save(User::factory()->viewAccessories()->make());
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userInCompanyA)
->getJson(route('api.accessories.index'))
->assertOk()
->assertResponseContainsInRows($accessoryA)
->assertResponseDoesNotContainInRows($accessoryB)
->assertResponseDoesNotContainInRows($accessoryC);
$this->actingAsForApi($userInCompanyB)
->getJson(route('api.accessories.index'))
->assertOk()
->assertResponseDoesNotContainInRows($accessoryA)
->assertResponseContainsInRows($accessoryB)
->assertResponseContainsInRows($accessoryC);
$this->actingAsForApi($superUser)
->getJson(route('api.accessories.index'))
->assertOk()
->assertResponseContainsInRows($accessoryA)
->assertResponseContainsInRows($accessoryB)
->assertResponseContainsInRows($accessoryC);
}
public function testCanGetAccessories()
{
$user = User::factory()->viewAccessories()->create();
$accessoryA = Accessory::factory()->create(['name' => 'Accessory A']);
$accessoryB = Accessory::factory()->create(['name' => 'Accessory B']);
$this->actingAsForApi($user)
->getJson(route('api.accessories.index'))
->assertOk()
->assertResponseContainsInRows($accessoryA)
->assertResponseContainsInRows($accessoryB);
}
} }

View file

@ -3,12 +3,15 @@
namespace Tests\Feature\Accessories\Api; namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Company;
use App\Models\User; use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase; use Tests\TestCase;
class ShowAccessoryTest extends TestCase class ShowAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{ {
public function testPermissionRequiredToShowAccessory() public function testRequiresPermission()
{ {
$accessory = Accessory::factory()->create(); $accessory = Accessory::factory()->create();
@ -16,4 +19,43 @@ class ShowAccessoryTest extends TestCase
->getJson(route('api.accessories.show', $accessory)) ->getJson(route('api.accessories.show', $accessory))
->assertForbidden(); ->assertForbidden();
} }
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$accessoryForCompanyA = Accessory::factory()->for($companyA)->create();
$superuser = User::factory()->superuser()->create();
$userForCompanyB = User::factory()->for($companyB)->viewAccessories()->create();
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userForCompanyB)
->getJson(route('api.accessories.show', $accessoryForCompanyA))
->assertOk()
->assertStatusMessageIs('error');
$this->actingAsForApi($superuser)
->getJson(route('api.accessories.show', $accessoryForCompanyA))
->assertOk()
->assertJsonFragment([
'id' => $accessoryForCompanyA->id,
]);
}
public function testCanGetSingleAccessory()
{
$accessory = Accessory::factory()->checkedOutToUser()->create(['name' => 'My Accessory']);
$this->actingAsForApi(User::factory()->viewAccessories()->create())
->getJson(route('api.accessories.show', $accessory))
->assertOk()
->assertJsonFragment([
'id' => $accessory->id,
'name' => 'My Accessory',
'checkouts_count' => 1,
]);
}
} }

View file

@ -2,15 +2,97 @@
namespace Tests\Feature\Accessories\Api; namespace Tests\Feature\Accessories\Api;
use App\Models\Category;
use App\Models\Company;
use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase; use Tests\TestCase;
class StoreAccessoryTest extends TestCase class StoreAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{ {
public function testPermissionRequiredToStoreAccessory() public function testRequiresPermission()
{ {
$this->actingAsForApi(User::factory()->create()) $this->actingAsForApi(User::factory()->create())
->postJson(route('api.accessories.store')) ->postJson(route('api.accessories.store'))
->assertForbidden(); ->assertForbidden();
} }
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
$this->markTestSkipped('This behavior is not implemented');
[$companyA, $companyB] = Company::factory()->count(2)->create();
$userInCompanyA = User::factory()->for($companyA)->createAccessories()->create();
$this->settings->enableMultipleFullCompanySupport();
// attempt to store an accessory for company B
$this->actingAsForApi($userInCompanyA)
->postJson(route('api.accessories.store'), [
'category_id' => Category::factory()->forAccessories()->create()->id,
'name' => 'My Awesome Accessory',
'qty' => 1,
'company_id' => $companyB->id,
])->assertStatusMessageIs('error');
$this->assertDatabaseMissing('accessories', [
'name' => 'My Awesome Accessory',
]);
}
public function testValidation()
{
$this->actingAsForApi(User::factory()->createAccessories()->create())
->postJson(route('api.accessories.store'), [
//
])
->assertStatusMessageIs('error')
->assertMessagesContains([
'category_id',
'name',
'qty',
]);
}
public function testCanStoreAccessory()
{
$category = Category::factory()->forAccessories()->create();
$company = Company::factory()->create();
$location = Location::factory()->create();
$manufacturer = Manufacturer::factory()->create();
$supplier = Supplier::factory()->create();
$this->actingAsForApi(User::factory()->createAccessories()->create())
->postJson(route('api.accessories.store'), [
'name' => 'My Awesome Accessory',
'qty' => 2,
'order_number' => '12345',
'purchase_cost' => 100.00,
'purchase_date' => '2024-09-18',
'model_number' => '98765',
'category_id' => $category->id,
'company_id' => $company->id,
'location_id' => $location->id,
'manufacturer_id' => $manufacturer->id,
'supplier_id' => $supplier->id,
])->assertStatusMessageIs('success');
$this->assertDatabaseHas('accessories', [
'name' => 'My Awesome Accessory',
'qty' => 2,
'order_number' => '12345',
'purchase_cost' => 100.00,
'purchase_date' => '2024-09-18',
'model_number' => '98765',
'category_id' => $category->id,
'company_id' => $company->id,
'location_id' => $location->id,
'manufacturer_id' => $manufacturer->id,
'supplier_id' => $supplier->id,
]);
}
} }

View file

@ -3,12 +3,19 @@
namespace Tests\Feature\Accessories\Api; namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Category;
use App\Models\Company;
use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase; use Tests\TestCase;
class UpdateAccessoryTest extends TestCase class UpdateAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{ {
public function testPermissionRequiredToUpdateAccessory() public function testRequiresPermission()
{ {
$accessory = Accessory::factory()->create(); $accessory = Accessory::factory()->create();
@ -16,4 +23,84 @@ class UpdateAccessoryTest extends TestCase
->patchJson(route('api.accessories.update', $accessory)) ->patchJson(route('api.accessories.update', $accessory))
->assertForbidden(); ->assertForbidden();
} }
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$accessoryA = Accessory::factory()->for($companyA)->create(['name' => 'A Name to Change']);
$accessoryB = Accessory::factory()->for($companyB)->create(['name' => 'A Name to Change']);
$accessoryC = Accessory::factory()->for($companyB)->create(['name' => 'A Name to Change']);
$superuser = User::factory()->superuser()->create();
$userInCompanyA = $companyA->users()->save(User::factory()->editAccessories()->make());
$userInCompanyB = $companyB->users()->save(User::factory()->editAccessories()->make());
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userInCompanyA)
->patchJson(route('api.accessories.update', $accessoryB), ['name' => 'New Name'])
->assertStatusMessageIs('error');
$this->actingAsForApi($userInCompanyB)
->patchJson(route('api.accessories.update', $accessoryA), ['name' => 'New Name'])
->assertStatusMessageIs('error');
$this->actingAsForApi($superuser)
->patchJson(route('api.accessories.update', $accessoryC), ['name' => 'New Name'])
->assertOk();
$this->assertEquals('A Name to Change', $accessoryA->fresh()->name);
$this->assertEquals('A Name to Change', $accessoryB->fresh()->name);
$this->assertEquals('New Name', $accessoryC->fresh()->name);
}
public function testCanUpdateAccessoryViaPatch()
{
[$categoryA, $categoryB] = Category::factory()->count(2)->create();
[$companyA, $companyB] = Company::factory()->count(2)->create();
[$locationA, $locationB] = Location::factory()->count(2)->create();
[$manufacturerA, $manufacturerB] = Manufacturer::factory()->count(2)->create();
[$supplierA, $supplierB] = Supplier::factory()->count(2)->create();
$accessory = Accessory::factory()->create([
'name' => 'A Name to Change',
'qty' => 5,
'order_number' => 'A12345',
'purchase_cost' => 99.99,
'model_number' => 'ABC098',
'category_id' => $categoryA->id,
'company_id' => $companyA->id,
'location_id' => $locationA->id,
'manufacturer_id' => $manufacturerA->id,
'supplier_id' => $supplierA->id,
]);
$this->actingAsForApi(User::factory()->editAccessories()->create())
->patchJson(route('api.accessories.update', $accessory), [
'name' => 'A New Name',
'qty' => 10,
'order_number' => 'B54321',
'purchase_cost' => 199.99,
'model_number' => 'XYZ123',
'category_id' => $categoryB->id,
'company_id' => $companyB->id,
'location_id' => $locationB->id,
'manufacturer_id' => $manufacturerB->id,
'supplier_id' => $supplierB->id,
])
->assertOk();
$accessory = $accessory->fresh();
$this->assertEquals('A New Name', $accessory->name);
$this->assertEquals(10, $accessory->qty);
$this->assertEquals('B54321', $accessory->order_number);
$this->assertEquals(199.99, $accessory->purchase_cost);
$this->assertEquals('XYZ123', $accessory->model_number);
$this->assertEquals($categoryB->id, $accessory->category_id);
$this->assertEquals($companyB->id, $accessory->company_id);
$this->assertEquals($locationB->id, $accessory->location_id);
$this->assertEquals($manufacturerB->id, $accessory->manufacturer_id);
$this->assertEquals($supplierB->id, $accessory->supplier_id);
}
} }

View file

@ -0,0 +1,86 @@
<?php
namespace Tests\Feature\Checkins\Api;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class AccessoryCheckinTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$accessory = Accessory::factory()->checkedOutToUser()->create();
$accessoryCheckout = $accessory->checkouts->first();
$this->actingAsForApi(User::factory()->create())
->postJson(route('api.accessories.checkin', $accessoryCheckout))
->assertForbidden();
}
public function testAdheresToFullMultipleCompaniesSupportScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$superUser = $companyA->users()->save(User::factory()->superuser()->make());
$userInCompanyA = User::factory()->for($companyA)->checkinAccessories()->create();
$accessoryForCompanyB = Accessory::factory()->for($companyB)->checkedOutToUser()->create();
$anotherAccessoryForCompanyB = Accessory::factory()->for($companyB)->checkedOutToUser()->create();
$this->assertEquals(1, $accessoryForCompanyB->checkouts->count());
$this->assertEquals(1, $anotherAccessoryForCompanyB->checkouts->count());
$this->settings->enableMultipleFullCompanySupport();
$this->actingAsForApi($userInCompanyA)
->postJson(route('api.accessories.checkin', $accessoryForCompanyB->checkouts->first()))
->assertForbidden();
$this->actingAsForApi($superUser)
->postJson(route('api.accessories.checkin', $anotherAccessoryForCompanyB->checkouts->first()))
->assertStatusMessageIs('success');
$this->assertEquals(1, $accessoryForCompanyB->fresh()->checkouts->count(), 'Accessory should not be checked in');
$this->assertEquals(0, $anotherAccessoryForCompanyB->fresh()->checkouts->count(), 'Accessory should be checked in');
}
public function testCanCheckinAccessory()
{
$accessory = Accessory::factory()->checkedOutToUser()->create();
$this->assertEquals(1, $accessory->checkouts->count());
$accessoryCheckout = $accessory->checkouts->first();
$this->actingAsForApi(User::factory()->checkinAccessories()->create())
->postJson(route('api.accessories.checkin', $accessoryCheckout))
->assertStatusMessageIs('success');
$this->assertEquals(0, $accessory->fresh()->checkouts->count(), 'Accessory should be checked in');
}
public function testCheckinIsLogged()
{
$user = User::factory()->create();
$actor = User::factory()->checkinAccessories()->create();
$accessory = Accessory::factory()->checkedOutToUser($user)->create();
$accessoryCheckout = $accessory->checkouts->first();
$this->actingAsForApi($actor)
->postJson(route('api.accessories.checkin', $accessoryCheckout))
->assertStatusMessageIs('success');
$this->assertDatabaseHas('action_logs', [
'created_by' => $actor->id,
'action_type' => 'checkin from',
'target_id' => $user->id,
'target_type' => User::class,
'item_id' => $accessory->id,
'item_type' => Accessory::class,
]);
}
}

View file

@ -7,11 +7,12 @@ use App\Models\Actionlog;
use App\Models\User; use App\Models\User;
use App\Notifications\CheckoutAccessoryNotification; use App\Notifications\CheckoutAccessoryNotification;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase; use Tests\TestCase;
class AccessoryCheckoutTest extends TestCase class AccessoryCheckoutTest extends TestCase implements TestsPermissionsRequirement
{ {
public function testCheckingOutAccessoryRequiresCorrectPermission() public function testRequiresPermission()
{ {
$this->actingAsForApi(User::factory()->create()) $this->actingAsForApi(User::factory()->create())
->postJson(route('api.accessories.checkout', Accessory::factory()->create())) ->postJson(route('api.accessories.checkout', Accessory::factory()->create()))

View file

@ -0,0 +1,16 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\User;
class GeneralImportTest extends ImportDataTestCase
{
public function testRequiresExistingImport()
{
$this->actingAsForApi(User::factory()->canImport()->create());
$this->importFileResponse(['import' => 9999, 'import-type' => 'accessory'])
->assertStatusMessageIs('import-errors');
}
}

View file

@ -0,0 +1,420 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Import;
use App\Models\User;
use Illuminate\Support\Str;
use PHPUnit\Framework\Attributes\Test;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Testing\TestResponse;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\AccessoriesImportFileBuilder as ImportFileBuilder;
use Tests\Support\Importing\CleansUpImportFiles;
class ImportAccessoriesTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'accessory';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAccessoryPermissionCanImportAccessories(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->accessory()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importAccessory(): void
{
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => [
'redirect_url' => route('accessories.index')
]
]);
$newAccessory = Accessory::query()
->with(['location', 'category', 'manufacturer', 'supplier', 'company'])
->where('name', $row['itemName'])
->sole();
$activityLog = Actionlog::query()
->where('item_type', Accessory::class)
->where('item_id', $newAccessory->id)
->sole();
$this->assertEquals('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newAccessory->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newAccessory->name);
$this->assertEquals($row['quantity'], $newAccessory->qty);
$this->assertEquals($row['purchaseDate'], $newAccessory->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newAccessory->purchase_cost);
$this->assertEquals($row['orderNumber'], $newAccessory->order_number);
$this->assertEquals($row['notes'], $newAccessory->notes);
$this->assertEquals($row['category'], $newAccessory->category->name);
$this->assertEquals('accessory', $newAccessory->category->category_type);
$this->assertEquals($row['manufacturerName'], $newAccessory->manufacturer->name);
$this->assertEquals($row['supplierName'], $newAccessory->supplier->name);
$this->assertEquals($row['location'], $newAccessory->location->name);
$this->assertEquals($row['companyName'], $newAccessory->company->name);
$this->assertEquals($row['modelNumber'], $newAccessory->model_number);
$this->assertFalse($newAccessory->requestable);
$this->assertNull($newAccessory->min_amt);
$this->assertNull($newAccessory->user_id);
}
#[Test]
public function whenImportFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumn'] = $this->faker->word;
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willFormatDate(): void
{
$importFileBuilder = ImportFileBuilder::new(['purchaseDate' => '2022/10/10']);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$accessory = Accessory::query()
->where('name', $importFileBuilder->firstRow()['itemName'])
->sole(['purchase_date']);
$this->assertEquals('2022-10-10', $accessory->purchase_date->toDateString());
}
#[Test]
public function willNotCreateNewCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessories = Accessory::query()
->whereIn('name', $importFileBuilder->pluck('itemName'))
->get();
$this->assertCount(1, $newAccessories->pluck('category_id')->unique()->all());
}
#[Test]
public function willNotCreateNewAccessoryWhenAccessoryWithNameExists(): void
{
$accessory = Accessory::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::times(2)->replace(['itemName' => $accessory->name]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['name']);
$this->assertCount(1, $probablyNewAccessories);
$this->assertEquals($accessory->name, $probablyNewAccessories->first()->name);
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyAlreadyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['company_id']);
$this->assertCount(1, $newAccessories->pluck('company_id')->unique()->all());
}
#[Test]
public function willNotCreateNewLocationWhenLocationAlreadyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['location_id']);
$this->assertCount(1, $newAccessories->pluck('location_id')->unique()->all());
}
#[Test]
public function willNotCreateNewManufacturerWhenManufacturerAlreadyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['manufacturer_id']);
$this->assertCount(1, $newAccessories->pluck('manufacturer_id')->unique()->all());
}
#[Test]
public function willNotCreateNewSupplierWhenSupplierAlreadyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['supplier_id']);
$this->assertCount(1, $newAccessories->pluck('supplier_id')->unique()->all());
}
#[Test]
public function whenColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new()->forget(['minimumAmount', 'purchaseCost', 'purchaseDate']);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAccessory = Accessory::query()
->where('name', $importFileBuilder->firstRow()['itemName'])
->sole();
$this->assertNull($newAccessory->min_amt);
$this->assertNull($newAccessory->purchase_date);
$this->assertNull($newAccessory->purchase_cost);
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new()->forget(['itemName', 'quantity', 'category']);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
'' => [
'Accessory' => [
'name' => ['The name field is required.'],
'qty' => ['The qty field must be at least 1.'],
'category_id' => ['The category id field is required.']
]
]
]
]);
}
#[Test]
public function updateAccessoryFromImport(): void
{
$accessory = Accessory::factory()->create(['name' => Str::random()])->refresh();
$importFileBuilder = ImportFileBuilder::new(['itemName' => $accessory->name]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedAccessory = Accessory::query()->find($accessory->id);
$updatedAttributes = [
'name', 'company_id', 'qty', 'purchase_date', 'purchase_cost',
'order_number', 'notes', 'category_id', 'manufacturer_id', 'supplier_id',
'location_id', 'model_number', 'updated_at'
];
$this->assertEquals($row['itemName'], $updatedAccessory->name);
$this->assertEquals($row['companyName'], $updatedAccessory->company->name);
$this->assertEquals($row['quantity'], $updatedAccessory->qty);
$this->assertEquals($row['purchaseDate'], $updatedAccessory->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedAccessory->purchase_cost);
$this->assertEquals($row['orderNumber'], $updatedAccessory->order_number);
$this->assertEquals($row['notes'], $updatedAccessory->notes);
$this->assertEquals($row['category'], $updatedAccessory->category->name);
$this->assertEquals('accessory', $updatedAccessory->category->category_type);
$this->assertEquals($row['manufacturerName'], $updatedAccessory->manufacturer->name);
$this->assertEquals($row['supplierName'], $updatedAccessory->supplier->name);
$this->assertEquals($row['location'], $updatedAccessory->location->name);
$this->assertEquals($row['modelNumber'], $updatedAccessory->model_number);
$this->assertEquals(
Arr::except($accessory->attributesToArray(), $updatedAttributes),
Arr::except($updatedAccessory->attributesToArray(), $updatedAttributes),
);
}
#[Test]
public function whenImportFileContainsEmptyValues(): void
{
$accessory = Accessory::factory()->create(['name' => Str::random()]);
$accessory->refresh();
$importFileBuilder = ImportFileBuilder::new([
'companyName' => ' ',
'purchaseDate' => ' ',
'purchaseCost' => '',
'location' => '',
'companyName' => '',
'orderNumber' => '',
'category' => '',
'quantity' => '',
'manufacturerName' => '',
'supplierName' => '',
'notes' => '',
'requestAble' => '',
'minimumAmount' => '',
'modelNumber' => ''
]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
$importFileBuilder->firstRow()['itemName'] => [
'Accessory' => [
'qty' => ['The qty field must be at least 1.'],
'category_id' => ['The category id field is required.']
]
]
]
]);
$importFileBuilder->replace(['itemName' => $accessory->name]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedAccessory = clone $accessory;
$updatedAccessory->refresh();
$this->assertEquals($accessory->toArray(), $updatedAccessory->toArray());
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::new()->definition();
$row = [
'itemName' => $faker['modelNumber'],
'purchaseDate' => $faker['notes'],
'purchaseCost' => $faker['location'],
'location' => $faker['purchaseCost'],
'companyName' => $faker['orderNumber'],
'orderNumber' => $faker['companyName'],
'category' => $faker['manufacturerName'],
'manufacturerName' => $faker['category'],
'notes' => $faker['purchaseDate'],
'minimumAmount' => $faker['supplierName'],
'modelNumber' => $faker['itemName'],
'quantity' => $faker['quantity']
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Item Name' => 'model_number',
'Purchase Date' => 'notes',
'Purchase Cost' => 'location',
'Location' => 'purchase_cost',
'Company' => 'order_number',
'Order Number' => 'company',
'Category' => 'manufacturer',
'Manufacturer' => 'category',
'Supplier' => 'min_amt',
'Notes' => 'purchase_date',
'Min QTY' => 'supplier',
'Model Number' => 'item_name',
'Quantity' => 'quantity'
]
])->assertOk();
$newAccessory = Accessory::query()
->with(['location', 'category', 'manufacturer', 'supplier'])
->where('name', $row['modelNumber'])
->sole();
$this->assertEquals($row['modelNumber'], $newAccessory->name);
$this->assertEquals($row['itemName'], $newAccessory->model_number);
$this->assertEquals($row['quantity'], $newAccessory->qty);
$this->assertEquals($row['notes'], $newAccessory->purchase_date->toDateString());
$this->assertEquals($row['location'], $newAccessory->purchase_cost);
$this->assertEquals($row['companyName'], $newAccessory->order_number);
$this->assertEquals($row['purchaseDate'], $newAccessory->notes);
$this->assertEquals($row['manufacturerName'], $newAccessory->category->name);
$this->assertEquals($row['category'], $newAccessory->manufacturer->name);
$this->assertEquals($row['purchaseCost'], $newAccessory->location->name);
}
}

View file

@ -0,0 +1,595 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Import;
use App\Models\User;
use App\Notifications\CheckoutAssetNotification;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\AssetsImportFileBuilder as ImportFileBuilder;
use Tests\Support\Importing\CleansUpImportFiles;
class ImportAssetsTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'asset';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportAssets(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->asset()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importAsset(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('hardware.index')]
]);
$newAsset = Asset::query()
->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer'])
->where('serial', $row['serialNumber'])
->sole();
$assignee = User::query()->find($newAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);
$activityLogs = ActionLog::query()
->where('item_type', Asset::class)
->where('item_id', $newAsset->id)
->get();
$this->assertCount(2, $activityLogs);
$this->assertEquals('checkout', $activityLogs[0]->action_type);
$this->assertEquals(Asset::class, $activityLogs[0]->item_type);
$this->assertEquals($assignee->id, $activityLogs[0]->target_id);
$this->assertEquals(User::class, $activityLogs[0]->target_type);
$this->assertEquals('Checkout from CSV Importer', $activityLogs[0]->note);
$this->assertEquals('create', $activityLogs[1]->action_type);
$this->assertNull($activityLogs[1]->target_id);
$this->assertEquals(Asset::class, $activityLogs[1]->item_type);
$this->assertNull($activityLogs[1]->note);
$this->assertNull($activityLogs[1]->target_type);
$this->assertEquals($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['assigneeEmail'], $assignee->email);
$this->assertEquals($row['assigneeUsername'], $assignee->username);
$this->assertEquals($row['category'], $newAsset->model->category->name);
$this->assertEquals($row['manufacturerName'], $newAsset->model->manufacturer->name);
$this->assertEquals($row['itemName'], $newAsset->name);
$this->assertEquals($row['tag'], $newAsset->asset_tag);
$this->assertEquals($row['model'], $newAsset->model->name);
$this->assertEquals($row['modelNumber'], $newAsset->model->model_number);
$this->assertEquals($row['purchaseDate'], $newAsset->purchase_date->toDateString());
$this->assertNull($newAsset->asset_eol_date);
$this->assertEquals(0, $newAsset->eol_explicit);
$this->assertEquals($newAsset->location_id, $newAsset->rtd_location_id);
$this->assertEquals($row['purchaseCost'], $newAsset->purchase_cost);
$this->assertNull($newAsset->order_number);
$this->assertEquals('', $newAsset->image);
$this->assertNull($newAsset->user_id);
$this->assertEquals(1, $newAsset->physical);
$this->assertEquals($row['status'], $newAsset->assetStatus->name);
$this->assertEquals(0, $newAsset->archived);
$this->assertEquals($row['warrantyInMonths'], $newAsset->warranty_months);
$this->assertNull($newAsset->deprecate);
$this->assertEquals($row['supplierName'], $newAsset->supplier->name);
$this->assertEquals(0, $newAsset->requestable);
$this->assertEquals($row['location'], $newAsset->defaultLoc->name);
$this->assertEquals(null, $newAsset->accepted);
$this->assertEquals(now()->toDateString(), Carbon::parse($newAsset->last_checkout)->toDateString());
$this->assertEquals(0, $newAsset->last_checkin);
$this->assertEquals(0, $newAsset->expected_checkin);
$this->assertEquals($row['companyName'], $newAsset->company->name);
$this->assertEquals(User::class, $newAsset->assigned_type);
$this->assertNull($newAsset->last_audit_date);
$this->assertNull($newAsset->next_audit_date);
$this->assertEquals($row['location'], $newAsset->location->name);
$this->assertEquals(0, $newAsset->checkin_counter);
$this->assertEquals(1, $newAsset->checkout_counter);
$this->assertEquals(0, $newAsset->requests_counter);
$this->assertEquals(0, $newAsset->byod);
//Notes is never read.
// $this->assertEquals($row['notes'], $newAsset->notes);
Notification::assertSentTo($assignee, CheckoutAssetNotification::class);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewAssetWhenAssetWithSameTagAlreadyExists(): void
{
$asset = Asset::factory()->create(['asset_tag' => $this->faker->uuid]);
$importFileBuilder = ImportFileBuilder::times(4)->replace(['tag' => $asset->asset_tag]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
'' => [
'asset_tag' => [
'asset_tag' => [
"An asset with the asset tag {$asset->asset_tag} already exists and an update was not requested. No change was made."
]
]
]
]
]);
$assetsWithSameTag = Asset::query()->where('asset_tag', $asset->asset_tag)->get();
$this->assertCount(1, $assetsWithSameTag);
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(1, $newAssets->pluck('company_id')->unique()->all());
}
#[Test]
public function willNotCreateNewLocationWhenLocationExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(1, $newAssets->pluck('location_id')->unique()->all());
}
#[Test]
public function willNotCreateNewSupplierWhenSupplierExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['supplier_id']);
$this->assertCount(1, $newAssets->pluck('supplier_id')->unique()->all());
}
#[Test]
public function willNotCreateNewManufacturerWhenManufacturerExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->with('model.manufacturer')
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(1, $newAssets->pluck('model.manufacturer_id')->unique()->all());
}
#[Test]
public function willNotCreateCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->with('model.category')
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(1, $newAssets->pluck('model.category_id')->unique()->all());
}
#[Test]
public function willNotCreateNewAssetModelWhenAssetModelExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['model' => Str::random()]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAssets = Asset::query()
->with('model')
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(1, $newAssets->pluck('model.name')->unique()->all());
}
#[Test]
public function whenColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::times()->forget([
'purchaseCost',
'purchaseDate',
'status'
]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()
->with(['assetStatus'])
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals('Ready to Deploy', $newAsset->assetStatus->name);
$this->assertNull($newAsset->purchase_date);
$this->assertNull($newAsset->purchase_cost);
}
#[Test]
public function willFormatValues(): void
{
$importFileBuilder = ImportFileBuilder::new([
'warrantyInMonths' => '3 months',
'purchaseDate' => '2022/10/10'
]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals(3, $newAsset->warranty_months);
$this->assertEquals('2022-10-10', $newAsset->purchase_date->toDateString());
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::times(2)
->forget(['tag'])
->replace(['model' => '']);
$rows = $importFileBuilder->all();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
$rows[0]['itemName'] => [
"Asset \"{$rows[0]['itemName']}\"" => [
'asset_tag' => [
'The asset tag field must be at least 1 characters.',
],
'model_id' => [
'The model id field is required.'
]
]
],
$rows[1]['itemName'] => [
"Asset \"{$rows[1]['itemName']}\"" => [
'asset_tag' => [
'The asset tag field must be at least 1 characters.',
],
'model_id' => [
'The model id field is required.'
]
]
]
]
]);
$newAssets = Asset::query()
->whereIn('serial', Arr::pluck($rows, 'serialNumber'))
->get();
$this->assertCount(0, $newAssets);
}
#[Test]
public function updateAssetFromImport(): void
{
$asset = Asset::factory()->create()->refresh();
$importFileBuilder = ImportFileBuilder::times(1)->replace(['tag' => $asset->asset_tag]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedAsset = Asset::query()
->with(['location', 'supplier', 'company', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer'])
->find($asset->id);
$assignee = User::query()->find($updatedAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);
$updatedAttributes = [
'category', 'manufacturer_id', 'name', 'tag', 'model_id',
'model_number', 'purchase_date', 'purchase_cost', 'warranty_months', 'supplier_id',
'location_id', 'company_id', 'serial', 'assigned_to', 'status_id', 'rtd_location_id',
'last_checkout', 'requestable', 'updated_at', 'checkout_counter', 'assigned_type'
];
$this->assertEquals($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['assigneeEmail'], $assignee->email);
$this->assertEquals($row['assigneeUsername'], $assignee->username);
$this->assertEquals($row['category'], $updatedAsset->model->category->name);
$this->assertEquals($row['manufacturerName'], $updatedAsset->model->manufacturer->name);
$this->assertEquals($row['itemName'], $updatedAsset->name);
$this->assertEquals($row['tag'], $updatedAsset->asset_tag);
$this->assertEquals($row['model'], $updatedAsset->model->name);
$this->assertEquals($row['modelNumber'], $updatedAsset->model->model_number);
$this->assertEquals($row['purchaseDate'], $updatedAsset->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedAsset->purchase_cost);
$this->assertEquals($row['status'], $updatedAsset->assetStatus->name);
$this->assertEquals($row['warrantyInMonths'], $updatedAsset->warranty_months);
$this->assertEquals($row['supplierName'], $updatedAsset->supplier->name);
$this->assertEquals($row['location'], $updatedAsset->defaultLoc->name);
$this->assertEquals($row['companyName'], $updatedAsset->company->name);
$this->assertEquals($row['location'], $updatedAsset->location->name);
$this->assertEquals(1, $updatedAsset->checkout_counter);
$this->assertEquals(user::class, $updatedAsset->assigned_type);
//RequestAble is always updated regardless of initial value.
// $this->assertEquals($asset->requestable, $updatedAsset->requestable);
$this->assertEquals(
Arr::except($asset->attributesToArray(), $updatedAttributes),
Arr::except($updatedAsset->attributesToArray(), $updatedAttributes),
);
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::new()->definition();
$row = [
'assigneeFullName' => $faker['supplierName'],
'assigneeEmail' => $faker['manufacturerName'],
'assigneeUsername' => $faker['serialNumber'],
'category' => $faker['location'],
'companyName' => $faker['purchaseCost'],
'itemName' => $faker['modelNumber'],
'location' => $faker['assigneeUsername'],
'manufacturerName' => $faker['status'],
'model' => $faker['itemName'],
'modelNumber' => $faker['category'],
'notes' => $faker['notes'],
'purchaseCost' => $faker['model'],
'purchaseDate' => $faker['companyName'],
'serialNumber' => $faker['tag'],
'supplierName' => $faker['purchaseDate'],
'status' => $faker['warrantyInMonths'],
'tag' => $faker['assigneeEmail'],
'warrantyInMonths' => $faker['assigneeFullName'],
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Asset Tag' => 'email',
'Category' => 'location',
'Company' => 'purchase_cost',
'Email' => 'manufacturer',
'Full Name' => 'supplier',
'Item Name' => 'model_number',
'Location' => 'username',
'Manufacturer' => 'status',
'Model name' => 'item_name',
'Model Number' => 'category',
'Notes' => 'asset_notes',
'Purchase Cost' => 'asset_model',
'Purchase Date' => 'company',
'Serial number' => 'asset_tag',
'Status' => 'warranty_months',
'Supplier' => 'purchase_date',
'Username' => 'serial',
'Warranty' => 'full_name',
]
])->assertOk();
$asset = Asset::query()
->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer'])
->where('serial', $row['assigneeUsername'])
->sole();
$assignee = User::query()->find($asset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);
$this->assertEquals($row['warrantyInMonths'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['tag'], $assignee->email);
$this->assertEquals($row['location'], $assignee->username);
$this->assertEquals($row['modelNumber'], $asset->model->category->name);
$this->assertEquals($row['assigneeEmail'], $asset->model->manufacturer->name);
$this->assertEquals($row['model'], $asset->name);
$this->assertEquals($row['serialNumber'], $asset->asset_tag);
$this->assertEquals($row['purchaseCost'], $asset->model->name);
$this->assertEquals($row['itemName'], $asset->model->model_number);
$this->assertEquals($row['supplierName'], $asset->purchase_date->toDateString());
$this->assertEquals($row['companyName'], $asset->purchase_cost);
$this->assertEquals($row['manufacturerName'], $asset->assetStatus->name);
$this->assertEquals($row['status'], $asset->warranty_months);
$this->assertEquals($row['assigneeFullName'], $asset->supplier->name);
$this->assertEquals($row['category'], $asset->defaultLoc->name);
$this->assertEquals($row['purchaseDate'], $asset->company->name);
$this->assertEquals($row['category'], $asset->location->name);
$this->assertEquals($row['notes'], $asset->notes);
$this->assertNull($asset->asset_eol_date);
$this->assertEquals(0, $asset->eol_explicit);
$this->assertNull($asset->order_number);
$this->assertEquals('', $asset->image);
$this->assertNull($asset->user_id);
$this->assertEquals(1, $asset->physical);
$this->assertEquals(0, $asset->archived);
$this->assertNull($asset->deprecate);
$this->assertEquals(0, $asset->requestable);
$this->assertEquals(null, $asset->accepted);
$this->assertEquals(now()->toDateString(), Carbon::parse($asset->last_checkout)->toDateString());
$this->assertEquals(0, $asset->last_checkin);
$this->assertEquals(0, $asset->expected_checkin);
$this->assertEquals(User::class, $asset->assigned_type);
$this->assertNull($asset->last_audit_date);
$this->assertNull($asset->next_audit_date);
$this->assertEquals(0, $asset->checkin_counter);
$this->assertEquals(1, $asset->checkout_counter);
$this->assertEquals(0, $asset->requests_counter);
$this->assertEquals(0, $asset->byod);
}
#[Test]
public function customFields(): void
{
$macAddress = $this->faker->macAddress;
$row = ImportFileBuilder::new()->definition();
$row['Mac Address'] = $macAddress;
$importFileBuilder = new ImportFileBuilder([$row]);
$customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew();
if (!$customField->exists) {
$customField = CustomField::factory()->macAddress()->create(['db_column' => '_snipeit_mac_address_1']);
}
if ($customField->field_encrypted) {
$customField->field_encrypted = 0;
$customField->save();
}
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
$this->assertEquals($macAddress, $newAsset->getAttribute($customField->db_column));
}
#[Test]
public function willEncryptCustomFields(): void
{
$macAddress = $this->faker->macAddress;
$row = ImportFileBuilder::new()->definition();
$row['Mac Address'] = $macAddress;
$importFileBuilder = new ImportFileBuilder([$row]);
$customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew();
if (!$customField->exists) {
$customField = CustomField::factory()->macAddress()->create();
}
if (!$customField->field_encrypted) {
$customField->field_encrypted = 1;
$customField->save();
}
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$asset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
$encryptedMacAddress = $asset->getAttribute($customField->db_column);
$this->assertNotEquals($encryptedMacAddress, $macAddress);
}
}

View file

@ -0,0 +1,305 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\Component;
use App\Models\Import;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\ComponentsImportFileBuilder as ImportFileBuilder;
class ImportComponentsTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'component';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportComponents(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->component()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importComponents(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('components.index')]
]);
$newComponent = Component::query()
->with(['location', 'category', 'company'])
->where('name', $row['itemName'])
->sole();
$activityLog = ActionLog::query()
->where('item_type', Component::class)
->where('item_id', $newComponent->id)
->sole();
$this->assertEquals('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newComponent->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newComponent->name);
$this->assertEquals($row['companyName'], $newComponent->company->name);
$this->assertEquals($row['category'], $newComponent->category->name);
$this->assertEquals($row['location'], $newComponent->location->name);
$this->assertNull($newComponent->supplier_id);
$this->assertEquals($row['quantity'], $newComponent->qty);
$this->assertEquals($row['orderNumber'], $newComponent->order_number);
$this->assertEquals($row['purchaseDate'], $newComponent->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newComponent->purchase_cost);
$this->assertNull($newComponent->min_amt);
$this->assertEquals($row['serialNumber'], $newComponent->serial);
$this->assertNull($newComponent->image);
$this->assertNull($newComponent->notes);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->firstRow();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewComponentWhenComponentWithNameAndSerialNumberExists(): void
{
$component = Component::factory()->create();
$importFileBuilder = ImportFileBuilder::times(4)->replace([
'itemName' => $component->name,
'serialNumber' => $component->serial
]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewComponents = Component::query()
->where('name', $component->name)
->where('serial', $component->serial)
->get(['id']);
$this->assertCount(1, $probablyNewComponents);
$this->assertEquals($component->id, $probablyNewComponents->sole()->id);
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newComponents = Component::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['company_id']);
$this->assertCount(1, $newComponents->pluck('company_id')->unique()->all());
}
#[Test]
public function willNotCreateNewLocationWhenLocationExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newComponents = Component::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['location_id']);
$this->assertCount(1, $newComponents->pluck('location_id')->unique()->all());
}
#[Test]
public function willNotCreateNewCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newComponents = Component::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['category_id']);
$this->assertCount(1, $newComponents->pluck('category_id')->unique()->all());
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new()
->replace(['category' => ''])
->forget(['quantity']);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
$row['itemName'] => [
'Component' => [
'qty' => ['The qty field must be at least 1.'],
'category_id' => ['The category id field is required.']
]
]
]
]);
$newComponents = Component::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get();
$this->assertCount(0, $newComponents);
}
#[Test]
public function updateComponentFromImport(): void
{
$component = Component::factory()->create();
$importFileBuilder = ImportFileBuilder::new([
'itemName' => $component->name,
'serialNumber' => $component->serial
]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedComponent = Component::query()
->with(['location', 'category'])
->where('serial', $row['serialNumber'])
->sole();
$this->assertEquals($row['itemName'], $updatedComponent->name);
$this->assertEquals($row['category'], $updatedComponent->category->name);
$this->assertEquals($row['location'], $updatedComponent->location->name);
$this->assertEquals($component->supplier_id, $updatedComponent->supplier_id);
$this->assertEquals($row['quantity'], $updatedComponent->qty);
$this->assertEquals($row['orderNumber'], $updatedComponent->order_number);
$this->assertEquals($row['purchaseDate'], $updatedComponent->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedComponent->purchase_cost);
$this->assertEquals($component->min_amt, $updatedComponent->min_amt);
$this->assertEquals($row['serialNumber'], $updatedComponent->serial);
$this->assertEquals($component->image, $updatedComponent->image);
$this->assertEquals($component->notes, $updatedComponent->notes);
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::new()->definition();
$row = [
'category' => $faker['serialNumber'],
'companyName' => $faker['quantity'],
'itemName' => $faker['purchaseDate'],
'location' => $faker['purchaseCost'],
'orderNumber' => $faker['orderNumber'],
'purchaseCost' => $faker['category'],
'purchaseDate' => $faker['companyName'],
'quantity' => $faker['itemName'],
'serialNumber' => $faker['location']
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Category' => 'serial',
'Company' => 'quantity',
'item Name' => 'purchase_date',
'Location' => 'purchase_cost',
'Order Number' => 'order_number',
'Purchase Cost' => 'category',
'Purchase Date' => 'company',
'Quantity' => 'item_name',
'Serial number' => 'location',
]
])->assertOk();
$newComponent = Component::query()
->with(['location', 'category'])
->where('serial', $importFileBuilder->firstRow()['category'])
->sole();
$this->assertEquals($row['quantity'], $newComponent->name);
$this->assertEquals($row['purchaseCost'], $newComponent->category->name);
$this->assertEquals($row['serialNumber'], $newComponent->location->name);
$this->assertNull($newComponent->supplier_id);
$this->assertEquals($row['companyName'], $newComponent->qty);
$this->assertEquals($row['orderNumber'], $newComponent->order_number);
$this->assertEquals($row['itemName'], $newComponent->purchase_date->toDateString());
$this->assertEquals($row['location'], $newComponent->purchase_cost);
$this->assertNull($newComponent->min_amt);
$this->assertNull($newComponent->image);
$this->assertNull($newComponent->notes);
}
}

View file

@ -0,0 +1,305 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActivityLog;
use App\Models\Consumable;
use App\Models\Import;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\ConsumablesImportFileBuilder as ImportFileBuilder;
class ImportConsumablesTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'consumable';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportConsumables(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->consumable()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importConsumables(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('consumables.index')]
]);
$newConsumable = Consumable::query()
->with(['location', 'category', 'company'])
->where('name', $row['itemName'])
->sole();
$activityLog = ActivityLog::query()
->where('item_type', Consumable::class)
->where('item_id', $newConsumable->id)
->sole();
$this->assertEquals('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newConsumable->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newConsumable->name);
$this->assertEquals($row['category'], $newConsumable->category->name);
$this->assertEquals($row['location'], $newConsumable->location->name);
$this->assertEquals($row['companyName'], $newConsumable->company->name);
$this->assertNull($newConsumable->supplier_id);
$this->assertFalse($newConsumable->requestable);
$this->assertNull($newConsumable->image);
$this->assertEquals($row['orderNumber'], $newConsumable->order_number);
$this->assertEquals($row['purchaseDate'], $newConsumable->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newConsumable->purchase_cost);
$this->assertNull($newConsumable->min_amt);
$this->assertEquals('', $newConsumable->model_number);
$this->assertNull($newConsumable->item_number);
$this->assertNull($newConsumable->manufacturer_id);
$this->assertNull($newConsumable->notes);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewConsumableWhenConsumableNameAlreadyExist(): void
{
$consumable = Consumable::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewConsumables = Consumable::query()
->where('name', $consumable->name)
->get();
$this->assertCount(1, $probablyNewConsumables);
$this->assertEquals($consumable->id, $probablyNewConsumables->sole()->id);
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newConsumables = Consumable::query()
->whereIn('name', $importFileBuilder->pluck('itemName'))
->get(['company_id']);
$this->assertCount(1, $newConsumables->pluck('company_id')->unique()->all());
}
#[Test]
public function willNotCreateNewLocationWhenLocationExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newConsumables = Consumable::query()
->whereIn('name', $importFileBuilder->pluck('itemName'))
->get(['location_id']);
$this->assertCount(1, $newConsumables->pluck('location_id')->unique()->all());
}
#[Test]
public function willNotCreateNewCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newConsumables = Consumable::query()
->whereIn('name', $importFileBuilder->pluck('itemName'))
->get(['category_id']);
$this->assertCount(1, $newConsumables->pluck('category_id')->unique()->all());
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new(['category' => ''])->forget(['quantity', 'name']);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
$row['itemName'] => [
'Consumable' => [
'category_id' => ['The category id field is required.']
]
]
]
]);
$newConsumables = Consumable::query()
->whereIn('name', $importFileBuilder->pluck('itemName'))
->get(['id']);
$this->assertCount(0, $newConsumables);
}
#[Test]
public function updateConsumableFromImport(): void
{
$consumable = Consumable::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedConsumable = Consumable::query()
->with(['location', 'category', 'company'])
->where('name', $importFileBuilder->firstRow()['itemName'])
->sole();
$this->assertEquals($row['itemName'], $updatedConsumable->name);
$this->assertEquals($row['category'], $updatedConsumable->category->name);
$this->assertEquals($row['location'], $updatedConsumable->location->name);
$this->assertEquals($row['companyName'], $updatedConsumable->company->name);
$this->assertEquals($row['orderNumber'], $updatedConsumable->order_number);
$this->assertEquals($row['purchaseDate'], $updatedConsumable->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedConsumable->purchase_cost);
$this->assertEquals($consumable->supplier_id, $updatedConsumable->supplier_id);
$this->assertEquals($consumable->requestable, $updatedConsumable->requestable);
$this->assertEquals($consumable->min_amt, $updatedConsumable->min_amt);
$this->assertEquals($consumable->model_number, $updatedConsumable->model_number);
$this->assertEquals($consumable->item_number, $updatedConsumable->item_number);
$this->assertEquals($consumable->manufacturer_id, $updatedConsumable->manufacturer_id);
$this->assertEquals($consumable->notes, $updatedConsumable->notes);
$this->assertEquals($consumable->item_number, $updatedConsumable->item_number);
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::new()->definition();
$row = [
'category' => $faker['supplier'],
'companyName' => $faker['quantity'],
'itemName' => $faker['purchaseDate'],
'location' => $faker['purchaseCost'],
'orderNumber' => $faker['orderNumber'],
'purchaseCost' => $faker['location'],
'purchaseDate' => $faker['companyName'],
'quantity' => $faker['itemName'],
'supplier' => $faker['category']
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Category' => 'supplier',
'Company' => 'quantity',
'item Name' => 'purchase_date',
'Location' => 'purchase_cost',
'Order Number' => 'order_number',
'Purchase Cost' => 'location',
'Purchase Date' => 'company',
'Quantity' => 'item_name',
'Supplier' => 'category',
]
])->assertOk();
$newConsumable = Consumable::query()
->with(['location', 'category', 'company'])
->where('name', $importFileBuilder->firstRow()['quantity'])
->sole();
$this->assertEquals($row['supplier'], $newConsumable->category->name);
$this->assertEquals($row['purchaseCost'], $newConsumable->location->name);
$this->assertEquals($row['purchaseDate'], $newConsumable->company->name);
$this->assertEquals($row['companyName'], $newConsumable->qty);
$this->assertEquals($row['quantity'], $newConsumable->name);
$this->assertNull($newConsumable->supplier_id);
$this->assertFalse($newConsumable->requestable);
$this->assertNull($newConsumable->image);
$this->assertEquals($row['orderNumber'], $newConsumable->order_number);
$this->assertEquals($row['itemName'], $newConsumable->purchase_date->toDateString());
$this->assertEquals($row['location'], $newConsumable->purchase_cost);
$this->assertNull($newConsumable->min_amt);
$this->assertEquals('', $newConsumable->model_number);
$this->assertNull($newConsumable->item_number);
$this->assertNull($newConsumable->manufacturer_id);
$this->assertNull($newConsumable->notes);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Tests\Feature\Importing\Api;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
abstract class ImportDataTestCase extends TestCase
{
protected function importFileResponse(array $parameters = []): TestResponse
{
return $this->postJson(route('api.imports.importFile', $parameters), $parameters);
}
}

View file

@ -0,0 +1,356 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActivityLog;
use App\Models\Import;
use App\Models\License;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\LicensesImportFileBuilder as ImportFileBuilder;
class ImportLicenseTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'license';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportLicenses(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->license()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importLicenses(): void
{
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('licenses.index')]
]);
$newLicense = License::query()
->withCasts(['reassignable' => 'bool'])
->with(['category', 'company', 'manufacturer', 'supplier'])
->where('serial', $row['serialNumber'])
->sole();
$activityLogs = ActivityLog::query()
->where('item_type', License::class)
->where('item_id', $newLicense->id)
->get();
$this->assertCount(2, $activityLogs);
$this->assertEquals($row['licenseName'], $newLicense->name);
$this->assertEquals($row['serialNumber'], $newLicense->serial);
$this->assertEquals($row['purchaseDate'], $newLicense->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newLicense->purchase_cost);
$this->assertEquals($row['orderNumber'], $newLicense->order_number);
$this->assertEquals($row['seats'], $newLicense->seats);
$this->assertEquals($row['notes'], $newLicense->notes);
$this->assertEquals($row['licensedToName'], $newLicense->license_name);
$this->assertEquals($row['licensedToEmail'], $newLicense->license_email);
$this->assertEquals($row['supplierName'], $newLicense->supplier->name);
$this->assertEquals($row['companyName'], $newLicense->company->name);
$this->assertEquals($row['category'], $newLicense->category->name);
$this->assertEquals($row['expirationDate'], $newLicense->expiration_date->toDateString());
$this->assertEquals($row['isMaintained'] === 'TRUE', $newLicense->maintained);
$this->assertEquals($row['isReassignAble'] === 'TRUE', $newLicense->reassignable);
$this->assertEquals('', $newLicense->purchase_order);
$this->assertNull($newLicense->depreciation_id);
$this->assertNull($newLicense->termination_date);
$this->assertNull($newLicense->deprecate);
$this->assertNull($newLicense->min_amt);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewLicenseWhenNameAndSerialNumberAlreadyExist(): void
{
$license = License::factory()->create();
$importFileBuilder = ImportFileBuilder::times(4)->replace([
'itemName' => $license->name,
'serialNumber' => $license->serial
]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewLicenses = License::query()
->where('name', $license->name)
->where('serial', $license->serial)
->get();
$this->assertCount(1, $probablyNewLicenses);
}
#[Test]
public function formatAttributes(): void
{
$importFileBuilder = ImportFileBuilder::new([
'expirationDate' => '2022/10/10'
]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newLicense = License::query()
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals('2022-10-10', $newLicense->expiration_date->toDateString());
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newLicenses = License::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['company_id']);
$this->assertCount(1, $newLicenses->pluck('company_id')->unique()->all());
}
#[Test]
public function willNotCreateNewManufacturerWhenManufacturerExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => Str::random()]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newLicenses = License::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['manufacturer_id']);
$this->assertCount(1, $newLicenses->pluck('manufacturer_id')->unique()->all());
}
#[Test]
public function willNotCreateNewCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newLicenses = License::query()
->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
->get(['category_id']);
$this->assertCount(1, $newLicenses->pluck('category_id')->unique()->all());
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::times()
->replace(['name' => ''])
->forget(['seats']);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
$row['licenseName'] => [
"License \"{$row['licenseName']}\"" => [
'seats' => ['The seats field is required.'],
]
]
]
]);
$newLicenses = License::query()
->where('serial', $row['serialNumber'])
->get();
$this->assertCount(0, $newLicenses);
}
#[Test]
public function updateLicenseFromImport(): void
{
$license = License::factory()->create();
$importFileBuilder = ImportFileBuilder::new([
'licenseName' => $license->name,
'serialNumber' => $license->serial
]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedLicense = License::query()
->with(['manufacturer', 'category', 'supplier'])
->where('serial', $row['serialNumber'])
->sole();
$this->assertEquals($row['licenseName'], $updatedLicense->name);
$this->assertEquals($row['serialNumber'], $updatedLicense->serial);
$this->assertEquals($row['purchaseDate'], $updatedLicense->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedLicense->purchase_cost);
$this->assertEquals($row['orderNumber'], $updatedLicense->order_number);
$this->assertEquals($row['seats'], $updatedLicense->seats);
$this->assertEquals($row['notes'], $updatedLicense->notes);
$this->assertEquals($row['licensedToName'], $updatedLicense->license_name);
$this->assertEquals($row['licensedToEmail'], $updatedLicense->license_email);
$this->assertEquals($row['supplierName'], $updatedLicense->supplier->name);
$this->assertEquals($row['companyName'], $updatedLicense->company->name);
$this->assertEquals($row['category'], $updatedLicense->category->name);
$this->assertEquals($row['expirationDate'], $updatedLicense->expiration_date->toDateString());
$this->assertEquals($row['isMaintained'] === 'TRUE', $updatedLicense->maintained);
$this->assertEquals($row['isReassignAble'] === 'TRUE', $updatedLicense->reassignable);
$this->assertEquals($license->purchase_order, $updatedLicense->purchase_order);
$this->assertEquals($license->depreciation_id, $updatedLicense->depreciation_id);
$this->assertEquals($license->termination_date, $updatedLicense->termination_date);
$this->assertEquals($license->deprecate, $updatedLicense->deprecate);
$this->assertEquals($license->min_amt, $updatedLicense->min_amt);
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::times()->definition();
$row = [
'category' => $faker['supplierName'],
'companyName' => $faker['serialNumber'],
'expirationDate' => $faker['seats'],
'isMaintained' => $faker['purchaseDate'],
'isReassignAble' => $faker['purchaseCost'],
'licensedToName' => $faker['orderNumber'],
'licensedToEmail' => $faker['notes'],
'licenseName' => $faker['licenseName'],
'manufacturerName' => $faker['category'],
'notes' => $faker['companyName'],
'orderNumber' => $faker['expirationDate'],
'purchaseCost' => $faker['isMaintained'],
'purchaseDate' => $faker['isReassignAble'],
'seats' => $faker['licensedToName'],
'serialNumber' => $faker['licensedToEmail'],
'supplierName' => $faker['manufacturerName']
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Category' => 'supplier',
'Company' => 'serial',
'expiration date' => 'seats',
'maintained' => 'purchase_date',
'reassignable' => 'purchase_cost',
'Licensed To Name' => 'order_number',
'Licensed To Email' => 'notes',
'licenseName' => 'name',
'manufacturer' => 'category',
'Notes' => 'company',
'Serial number' => 'license_email',
'Order Number' => 'expiration_date',
'purchase Cost' => 'maintained',
'purchase Date' => 'reassignable',
'seats' => 'license_name',
'supplier' => 'manufacturer'
]
])->assertOk();
$newLicense = License::query()
->with(['category', 'company', 'manufacturer', 'supplier'])
->where('serial', $row['companyName'])
->sole();
$this->assertEquals($row['licenseName'], $newLicense->name);
$this->assertEquals($row['companyName'], $newLicense->serial);
$this->assertEquals($row['isMaintained'], $newLicense->purchase_date->toDateString());
$this->assertEquals($row['isReassignAble'], $newLicense->purchase_cost);
$this->assertEquals($row['licensedToName'], $newLicense->order_number);
$this->assertEquals($row['expirationDate'], $newLicense->seats);
$this->assertEquals($row['licensedToEmail'], $newLicense->notes);
$this->assertEquals($row['seats'], $newLicense->license_name);
$this->assertEquals($row['serialNumber'], $newLicense->license_email);
$this->assertEquals($row['category'], $newLicense->supplier->name);
$this->assertEquals($row['notes'], $newLicense->company->name);
$this->assertEquals($row['manufacturerName'], $newLicense->category->name);
$this->assertEquals($row['orderNumber'], $newLicense->expiration_date->toDateString());
$this->assertEquals($row['purchaseCost'] === 'TRUE', $newLicense->maintained);
$this->assertEquals($row['purchaseDate'] === 'TRUE', $newLicense->reassignable);
$this->assertEquals('', $newLicense->purchase_order);
$this->assertNull($newLicense->depreciation_id);
$this->assertNull($newLicense->termination_date);
$this->assertNull($newLicense->deprecate);
$this->assertNull($newLicense->min_amt);
}
}

View file

@ -0,0 +1,336 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Asset;
use App\Models\Import;
use App\Models\Location;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\UsersImportFileBuilder as ImportFileBuilder;
class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'user';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportUsers(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->users()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importUsers(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'send-welcome' => 1])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('users.index')]
]);
$newUser = User::query()
->with(['company', 'location'])
->where('username', $row['username'])
->sole();
Notification::assertNothingSent();
$this->assertEquals($row['email'], $newUser->email);
$this->assertEquals($row['firstName'], $newUser->first_name);
$this->assertEquals($row['lastName'], $newUser->last_name);
$this->assertEquals($row['employeeNumber'], $newUser->employee_num);
$this->assertEquals($row['companyName'], $newUser->company->name);
$this->assertEquals($row['location'], $newUser->location->name);
$this->assertEquals($row['phoneNumber'], $newUser->phone);
$this->assertEquals($row['position'], $newUser->jobtitle);
$this->assertTrue(Hash::isHashed($newUser->password));
$this->assertEquals('', $newUser->website);
$this->assertEquals('', $newUser->country);
$this->assertEquals('', $newUser->address);
$this->assertEquals('', $newUser->city);
$this->assertEquals('', $newUser->state);
$this->assertEquals('', $newUser->zip);
$this->assertNull($newUser->permissions);
$this->assertNull($newUser->avatar);
$this->assertNull($newUser->notes);
$this->assertNull($newUser->skin);
$this->assertNull($newUser->department_id);
$this->assertNull($newUser->two_factor_secret);
$this->assertNull($newUser->idap_import);
$this->assertEquals('en-US', $newUser->locale);
$this->assertEquals(1, $newUser->show_in_list);
$this->assertEquals(0, $newUser->two_factor_enrolled);
$this->assertEquals(0, $newUser->two_factor_optin);
$this->assertEquals(0, $newUser->remote);
$this->assertEquals(0, $newUser->autoassign_licenses);
$this->assertEquals(0, $newUser->vip);
$this->assertEquals(0, $newUser->enable_sounds);
$this->assertEquals(0, $newUser->enable_confetti);
$this->assertNull($newUser->created_by);
$this->assertNull($newUser->start_date);
$this->assertNull($newUser->end_date);
$this->assertNull($newUser->scim_externalid);
$this->assertNull($newUser->manager_id);
$this->assertNull($newUser->activation_code);
$this->assertNull($newUser->last_login);
$this->assertNull($newUser->persist_code);
$this->assertNull($newUser->reset_password_code);
$this->assertEquals(0, $newUser->activated);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewUserWhenUserWithUserNameAlreadyExist(): void
{
$user = User::factory()->create(['username' => Str::random()]);
$importFileBuilder = ImportFileBuilder::times(4)->replace(['username' => $user->username]);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewUsers = User::query()
->where('username', $user->username)
->get();
$this->assertCount(1, $probablyNewUsers);
}
#[Test]
public function willGenerateUsernameWhenUsernameFieldIsMissing(): void
{
$importFileBuilder = ImportFileBuilder::new()->forget('username');
$row = $importFileBuilder->firstRow();
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newUser = User::query()
->where('email', $row['email'])
->sole();
$generatedUsername = User::generateFormattedNameFromFullName("{$row['firstName']} {$row['lastName']}")['username'];
$this->assertEquals($generatedUsername, $newUser->username);
}
#[Test]
public function willUpdateLocationOfAllAssetsAssignedToUser(): void
{
$user = User::factory()->create(['username' => Str::random()]);
$assetsAssignedToUser = Asset::factory()->create(['assigned_to' => $user->id, 'assigned_type' => User::class]);
$importFileBuilder = ImportFileBuilder::new(['username' => $user->username]);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$userLocation = Location::query()->where('name', $importFileBuilder->firstRow()['location'])->sole(['id']);
$this->assertEquals(
$userLocation->id,
$assetsAssignedToUser->refresh()->location_id
);
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new(['firstName' => ''])->forget(['username']);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])
->assertInternalServerError()
->assertExactJson([
'status' => 'import-errors',
'payload' => null,
'messages' => [
'' => [
'User' => [
'first_name' => ['The first name field is required.'],
]
]
]
]);
$newUsers = User::query()
->where('email', $importFileBuilder->firstRow()['email'])
->get();
$this->assertCount(0, $newUsers);
}
#[Test]
public function updateUserFromImport(): void
{
$user = User::factory()->create(['username' => Str::random()])->refresh();
$importFileBuilder = ImportFileBuilder::new(['username' => $user->username]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedUser = User::query()->with(['company', 'location'])->find($user->id);
$updatedAttributes = [
'first_name', 'email', 'last_name', 'employee_num', 'company',
'location_id', 'company_id', 'updated_at', 'phone', 'jobtitle'
];
$this->assertEquals($row['email'], $updatedUser->email);
$this->assertEquals($row['firstName'], $updatedUser->first_name);
$this->assertEquals($row['lastName'], $updatedUser->last_name);
$this->assertEquals($row['employeeNumber'], $updatedUser->employee_num);
$this->assertEquals($row['companyName'], $updatedUser->company->name);
$this->assertEquals($row['location'], $updatedUser->location->name);
$this->assertEquals($row['phoneNumber'], $updatedUser->phone);
$this->assertEquals($row['position'], $updatedUser->jobtitle);
$this->assertTrue(Hash::isHashed($updatedUser->password));
$this->assertEquals(
Arr::except($user->attributesToArray(), $updatedAttributes),
Arr::except($updatedUser->attributesToArray(), $updatedAttributes),
);
}
#[Test]
public function customColumnMapping(): void
{
$faker = ImportFileBuilder::new()->definition();
$row = [
'companyName' => $faker['username'],
'email' => $faker['position'],
'employeeNumber' => $faker['phoneNumber'],
'firstName' => $faker['location'],
'lastName' => $faker['lastName'],
'location' => $faker['firstName'],
'phoneNumber' => $faker['employeeNumber'],
'position' => $faker['email'],
'username' => $faker['companyName'],
];
$importFileBuilder = new ImportFileBuilder([$row]);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse([
'import' => $import->id,
'column-mappings' => [
'Company' => 'username',
'email' => 'jobtitle',
'Employee Number' => 'phone_number',
'First Name' => 'location',
'Last Name' => 'last_name',
'Location' => 'first_name',
'Phone Number' => 'employee_num',
'Job Title' => 'email',
'Username' => 'company',
]
])->assertOk();
$newUser = User::query()
->with(['company', 'location'])
->where('username', $row['companyName'])
->sole();
$this->assertEquals($row['position'], $newUser->email);
$this->assertEquals($row['location'], $newUser->first_name);
$this->assertEquals($row['lastName'], $newUser->last_name);
$this->assertEquals($row['email'], $newUser->jobtitle);
$this->assertEquals($row['phoneNumber'], $newUser->employee_num);
$this->assertEquals($row['username'], $newUser->company->name);
$this->assertEquals($row['firstName'], $newUser->location->name);
$this->assertEquals($row['employeeNumber'], $newUser->phone);
$this->assertTrue(Hash::isHashed($newUser->password));
$this->assertEquals('', $newUser->website);
$this->assertEquals('', $newUser->country);
$this->assertEquals('', $newUser->address);
$this->assertEquals('', $newUser->city);
$this->assertEquals('', $newUser->state);
$this->assertEquals('', $newUser->zip);
$this->assertNull($newUser->permissions);
$this->assertNull($newUser->avatar);
$this->assertNull($newUser->notes);
$this->assertNull($newUser->skin);
$this->assertNull($newUser->department_id);
$this->assertNull($newUser->two_factor_secret);
$this->assertNull($newUser->idap_import);
$this->assertEquals('en-US', $newUser->locale);
$this->assertEquals(1, $newUser->show_in_list);
$this->assertEquals(0, $newUser->two_factor_enrolled);
$this->assertEquals(0, $newUser->two_factor_optin);
$this->assertEquals(0, $newUser->remote);
$this->assertEquals(0, $newUser->autoassign_licenses);
$this->assertEquals(0, $newUser->vip);
$this->assertEquals(0, $newUser->enable_sounds);
$this->assertEquals(0, $newUser->enable_confetti);
$this->assertNull($newUser->created_by);
$this->assertNull($newUser->start_date);
$this->assertNull($newUser->end_date);
$this->assertNull($newUser->scim_externalid);
$this->assertNull($newUser->manager_id);
$this->assertNull($newUser->activation_code);
$this->assertNull($newUser->last_login);
$this->assertNull($newUser->persist_code);
$this->assertNull($newUser->reset_password_code);
$this->assertEquals(0, $newUser->activated);
}
}

View file

@ -2,22 +2,28 @@
namespace Tests\Feature\Settings; namespace Tests\Feature\Settings;
use App\Models\Asset;
use Tests\TestCase; use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Models\User; use App\Models\User;
use App\Models\Setting;
class AlertsSettingTest extends TestCase class AlertsSettingTest extends TestCase
{ {
public function testPermissionRequiredToViewAlertSettings() public function testPermissionRequiredToViewAlertSettings()
{ {
$asset = Asset::factory()->create();
$this->actingAs(User::factory()->create()) $this->actingAs(User::factory()->create())
->get(route('settings.alerts.index')) ->get(route('settings.alerts.index'))
->assertForbidden(); ->assertForbidden();
} }
public function testAdminCCEmailArrayCanBeSaved()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('settings.alerts.save', ['alert_email' => 'me@example.com,you@example.com']))
->assertStatus(302)
->assertValid('alert_email')
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$this->followRedirects($response)->assertSee('alert-success');
}
} }

View file

@ -2,7 +2,6 @@
namespace Tests\Feature\Settings; namespace Tests\Feature\Settings;
use App\Models\Asset;
use Tests\TestCase; use Tests\TestCase;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;

View file

@ -0,0 +1,18 @@
<?php
namespace Tests\Feature\Settings;
use Tests\TestCase;
use App\Models\User;
class LabelSettingTest extends TestCase
{
public function testPermissionRequiredToViewLabelSettings()
{
$this->actingAs(User::factory()->create())
->get(route('settings.labels.index'))
->assertForbidden();
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Tests\Feature\Settings;
use Tests\TestCase;
use App\Models\User;
class LdapSettingsTest extends TestCase
{
public function testPermissionRequiredToViewLdapSettings()
{
$this->actingAs(User::factory()->create())
->get(route('settings.ldap.index'))
->assertForbidden();
}
public function testLdapSettingsCanBeSaved()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('settings.ldap.save', [
'ldap_enabled' => 1,
'ldap_username_field' => 'samaccountname',
'ldap_filter' => 'uid=',
'ldap_auth_filter_query' => 'uid=',
'ldap_uname' => 'SomeUserField',
'ldap_pword' => 'MyAwesomePassword',
'ldap_basedn' => 'uid=',
'ldap_fname_field' => 'SomeFirstnameField',
'ldap_server' => 'ldaps://ldap.example.com',
]))
->assertStatus(302)
->assertValid('ldap_enabled')
->assertRedirect(route('settings.ldap.index'))
->assertSessionHasNoErrors();
$this->followRedirects($response)->assertSee('alert-success');
}
public function testLdapSettingsAreValidatedCorrectly()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->from(route('settings.ldap.index'))
->post(route('settings.ldap.save', [
'ldap_enabled' => 1,
'ldap_username_field' => 'sAMAccountName',
'ldap_filter' => '(uid=)',
]))
->assertStatus(302)
->assertRedirect(route('settings.ldap.index'))
->assertSessionHasErrors([
'ldap_username_field',
'ldap_auth_filter_query',
'ldap_uname',
'ldap_pword',
'ldap_basedn',
'ldap_fname_field',
'ldap_server',
]);
$this->followRedirects($response)->assertSee('alert-danger');
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Tests\Feature\Settings;
use Tests\TestCase;
use App\Models\User;
class SecuritySettingTest extends TestCase
{
public function testPermissionRequiredToViewSecuritySettings()
{
$this->actingAs(User::factory()->create())
->get(route('settings.security.index'))
->assertForbidden();
}
}

View file

@ -100,5 +100,26 @@ trait CustomTestMacros
return $this; return $this;
} }
); );
TestResponse::macro(
'assertMessagesContains',
function (array|string $keys) {
Assert::assertArrayHasKey('messages', $this, 'Response did not contain any messages');
if (is_string($keys)) {
$keys = [$keys];
}
foreach ($keys as $key) {
Assert::assertArrayHasKey(
$key,
$this['messages'],
"Response messages did not contain the key: {$key}"
);
}
return $this;
}
);
} }
} }

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build an accessories import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* manufacturerName?: string,
* modelNumber?: string,
* notes?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* supplierName?: string
* }
*
* @extends FileBuilder<Row>
*/
class AccessoriesImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'category' => 'Category',
'companyName' => 'Company',
'itemName' => 'Item Name',
'location' => 'Location',
'manufacturerName' => 'Manufacturer',
'modelNumber' => 'Model Number',
'notes' => 'Notes',
'orderNumber' => 'Order Number',
'purchaseCost' => 'Purchase Cost',
'purchaseDate' => 'Purchase Date',
'quantity' => 'Quantity',
'supplierName' => 'Supplier',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'category' => Str::random(),
'companyName' => Str::random(),
'itemName' => Str::random(),
'location' => "{$faker->city}, {$faker->country}",
'manufacturerName' => $faker->company,
'modelNumber' => Str::random(),
'notes' => $faker->sentence,
'orderNumber' => Str::random(),
'purchaseDate' => $faker->date(),
'purchaseCost' => rand(1, 100),
'quantity' => rand(1, 100),
'supplierName' => $faker->company,
];
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build an assets import file at runtime for testing.
*
* @template Row of array{
* assigneeFullName?: string,
* assigneeEmail?: string,
* assigneeUsername?: string,
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* manufacturerName?: int,
* model?: string,
* modelNumber?: string,
* notes?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* serialNumber?: string,
* supplierName?: string,
* status?: string,
* tag?: string,
* warrantyInMonths?: int,
* }
*
* @extends FileBuilder<Row>
*/
class AssetsImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'assigneeFullName' => 'Full Name',
'assigneeEmail' => 'Email',
'assigneeUsername' => 'Username',
'category' => 'Category',
'companyName' => 'Company',
'itemName' => 'item Name',
'location' => 'Location',
'manufacturerName' => 'Manufacturer',
'model' => 'Model name',
'modelNumber' => 'Model Number',
'notes' => 'Notes',
'purchaseCost' => 'Purchase Cost',
'purchaseDate' => 'Purchase Date',
'serialNumber' => 'Serial number',
'supplierName' => 'Supplier',
'status' => 'Status',
'tag' => 'Asset Tag',
'warrantyInMonths' => 'Warranty',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'assigneeFullName' => $faker->name,
'assigneeEmail' => $faker->email,
'assigneeUsername' => $faker->userName,
'category' => Str::random(),
'companyName' => Str::random() . " {$faker->companySuffix}",
'itemName' => Str::random(),
'location' => "{$faker->country},{$faker->city}",
'manufacturerName' => $faker->company,
'model' => Str::random(),
'modelNumber' => Str::random(),
'notes' => $faker->sentence(5),
'purchaseCost' => rand(1, 100_000),
'purchaseDate' => $faker->date,
'serialNumber' => $faker->uuid,
'supplierName' => $faker->company,
'status' => $faker->randomElement(['Ready to Deploy', 'Archived', 'Pending']),
'tag' => Str::random(),
'warrantyInMonths' => rand(1, 12),
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Tests\Support\Importing;
use App\Models\Import;
use Illuminate\Support\Facades\Storage;
trait CleansUpImportFiles
{
public function setUp(): void
{
parent::setUp();
Import::created(function (Import $import) {
$this->beforeApplicationDestroyed(function () use ($import) {
Storage::delete('private_uploads/imports/' . $import->file_path);
});
});
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a components import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* serialNumber?: string,
* }
*
* @extends FileBuilder<Row>
*/
class ComponentsImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'category' => 'Category',
'companyName' => 'Company',
'itemName' => 'item Name',
'location' => 'Location',
'orderNumber' => 'Order Number',
'purchaseCost' => 'Purchase Cost',
'purchaseDate' => 'Purchase Date',
'quantity' => 'Quantity',
'serialNumber' => 'Serial number',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'category' => Str::random(),
'companyName' => Str::random() . " {$faker->companySuffix}",
'itemName' => Str::random(),
'location' => "{$faker->city}, {$faker->country}",
'orderNumber' => "ON:COM:{$faker->uuid}",
'purchaseCost' => rand(1, 100_000),
'purchaseDate' => $faker->date,
'quantity' => rand(1, 100_000),
'serialNumber' => 'SN:COM:' . Str::random(),
];
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a consumables import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* supplier?: string,
* }
*
* @extends FileBuilder<Row>
*/
class ConsumablesImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'category' => 'Category',
'companyName' => 'Company',
'itemName' => 'item Name',
'location' => 'Location',
'orderNumber' => 'Order Number',
'purchaseCost' => 'Purchase Cost',
'purchaseDate' => 'Purchase Date',
'quantity' => 'Quantity',
'supplier' => 'Supplier',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'category' => Str::random(),
'companyName' => Str::random() . " {$faker->companySuffix}",
'itemName' => Str::random(),
'location' => "{$faker->city}, {$faker->country}",
'orderNumber' => "ON:CON:{$faker->uuid}",
'purchaseCost' => rand(1, 100_000),
'purchaseDate' => $faker->date,
'quantity' => rand(1, 100_000),
'supplier' => Str::random() . " {$faker->companySuffix}",
];
}
}

View file

@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use League\Csv\Reader;
use OutOfBoundsException;
/**
* @template Row of array
*/
abstract class FileBuilder
{
/**
* The import file rows.
*
* @var Collection<Row>
*/
protected Collection $rows;
/**
* Define the builders default row.
*
* @return Row
*/
abstract public function definition();
/**
* @param array<Row> $rows
*/
public function __construct(array $rows = [])
{
$this->rows = new Collection($rows);
}
/**
* Get a new file builder instance.
*
* @param Row $attributes
*
* @return static
*/
public static function new(array $attributes = [])
{
$instance = new static;
return $instance->push($instance->definition())->replace($attributes);
}
/**
* Get a new file builder instance from an import file.
*
* @return static
*/
public static function fromFile(string $filepath)
{
$instance = new static;
$reader = Reader::createFromPath($filepath);
$importFileHeaders = $reader->first();
$dictionary = array_flip($instance->getDictionary());
foreach ($reader->getRecords() as $key => $record) {
$row = [];
//Skip header.
if ($key === 0) {
continue;
}
foreach ($record as $index => $value) {
$columnNameInImportFile = $importFileHeaders[$index];
//Try to map the value to a dictionary or use the file's
//column if the key is not defined in the dictionary.
$row[$dictionary[$columnNameInImportFile] ?? $columnNameInImportFile] = $value;
}
$instance->push($row);
}
return $instance;
}
/**
* Get a new builder instance for the given number of rows.
*
* @return static
*/
public static function times(int $amountOfRows = 1)
{
$instance = new static;
for ($i = 1; $i <= $amountOfRows; $i++) {
$instance->push($instance->definition());
}
return $instance;
}
/**
* The the dictionary for mapping row keys to the corresponding import file headers.
*
* @return array<string,string>
*/
protected function getDictionary(): array
{
return [];
}
/**
* Add a new row.
*
* @param Row $row
*
* @return $this
*/
public function push(array $row)
{
if (!empty($row)) {
$this->rows->push($row);
}
return $this;
}
/**
* Pluck an array of values from the rows.
*/
public function pluck(string $key): array
{
return $this->rows->pluck($key)->all();
}
/**
* Replace the keys in each row with the values of the given replacement if they exist.
*
* @param array<Row> $replacement
*
* @return $this
*/
public function replace(array $replacement)
{
$this->rows = $this->rows->map(function (array $row) use ($replacement) {
foreach ($replacement as $key => $value) {
if (!array_key_exists($key, $row)) {
continue;
}
$row[$key] = $value;
}
return $row;
});
return $this;
}
/**
* Remove the the given keys from all rows.
*
* @param string|array<string> $keys
*
* @return $this
*/
public function forget(array|string $keys)
{
$keys = (array) $keys;
$this->rows = $this->rows->map(function (array $row) use ($keys) {
foreach ($keys as $key) {
unset($row[$key]);
}
return $row;
});
return $this;
}
public function toCsv(): array
{
if ($this->rows->isEmpty()) {
return [];
}
$headers = [];
$rows = $this->rows;
$dictionary = $this->getDictionary();
foreach (array_keys($rows->first()) as $key) {
$headers[] = $dictionary[$key] ?? $key;
}
return $rows
->map(fn (array $row) => array_values(array_combine($headers, $row)))
->prepend($headers)
->all();
}
/**
* Save the rows to the imports folder as a csv file.
*
* @return string The filename.
*/
public function saveToImportsDirectory(?string $filename = null): string
{
$filename ??= Str::random(40) . '.csv';
try {
$stream = fopen(config('app.private_uploads') . "/imports/{$filename}", 'w');
foreach ($this->toCsv() as $row) {
fputcsv($stream, $row);
}
return $filename;
} finally {
if (is_resource($stream)) {
fclose($stream);
}
}
}
/**
* Get the first row of the import file.
*
* @throws OutOfBoundsException
*
* @return Row
*/
public function firstRow(): array
{
return $this->rows->first(null, fn () => throw new OutOfBoundsException('Could not retrieve row from collection.'));
}
/**
* Get the all the rows of the import file.
*
* @return array<Row>
*/
public function all(): array
{
return $this->rows->all();
}
}

View file

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a consumables import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* expirationDate?: string,
* isMaintained?: bool,
* isReassignAble?: bool,
* licensedToName?: string,
* licensedToEmail?: email,
* licenseName?: string,
* manufacturerName?: string,
* notes?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* seats?: int,
* serialNumber?: string,
* supplierName?: string
* }
*
* @extends FileBuilder<Row>
*/
class LicensesImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'category' => 'Category',
'companyName' => 'Company',
'expirationDate' => 'expiration date',
'isMaintained' => 'maintained',
'isReassignAble' => 'reassignable',
'licensedToName' => 'Licensed To Name',
'licensedToEmail' => 'Licensed to Email',
'licenseName' => 'Item name',
'manufacturerName' => 'manufacturer',
'notes' => 'notes',
'orderNumber' => 'Order Number',
'purchaseCost' => 'Purchase Cost',
'purchaseDate' => 'Purchase Date',
'seats' => 'seats',
'serialNumber' => 'Serial number',
'supplierName' => 'supplier',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'category' => Str::random(),
'companyName' => Str::random() . " {$faker->companySuffix}",
'expirationDate' => $faker->date,
'isMaintained' => $faker->randomElement(['TRUE', 'FALSE']),
'isReassignAble' => $faker->randomElement(['TRUE', 'FALSE']),
'licensedToName' => $faker->name,
'licensedToEmail' => $faker->email,
'licenseName' => $faker->company,
'manufacturerName' => $faker->company,
'notes' => $faker->sentence,
'orderNumber' => "ON:LIC:{$faker->uuid}",
'purchaseCost' => rand(1, 100_000),
'purchaseDate' => $faker->date,
'seats' => rand(1, 10),
'serialNumber' => 'SN:LIC:' . Str::random(),
'supplierName' => $faker->company,
];
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a users import file at runtime for testing.
*
* @template Row of array{
* companyName?: string,
* email?: string,
* employeeNumber?: int,
* firstName?: string,
* lastName?: string,
* location?: string,
* phoneNumber?: string,
* position?: string,
* username?: string,
* }
*
* @extends FileBuilder<Row>
*/
class UsersImportFileBuilder extends FileBuilder
{
/**
* @inheritdoc
*/
protected function getDictionary(): array
{
return [
'companyName' => 'Company',
'email' => 'email',
'employeeNumber' => 'Employee Number',
'firstName' => 'First Name',
'lastName' => 'Last Name',
'location' => 'Location',
'phoneNumber' => 'Phone Number',
'position' => 'Job Title',
'username' => 'Username',
];
}
/**
* @inheritdoc
*/
public function definition(): array
{
$faker = fake();
return [
'companyName' => $faker->company,
'email' => Str::random(32) . "@{$faker->freeEmailDomain}",
'employeeNumber' => $faker->uuid,
'firstName' => $faker->firstName,
'lastName' => $faker->lastName,
'location' => "{$faker->city}, {$faker->country}",
'phoneNumber' => $faker->phoneNumber,
'position' => $faker->jobTitle,
'username' => Str::random(),
];
}
}

View file

@ -96,7 +96,7 @@ class LdapTest extends TestCase
"count" => 1, "count" => 1,
0 => [ 0 => [
'sn' => 'Surname', 'sn' => 'Surname',
'firstName' => 'FirstName' 'firstname' => 'FirstName'
] ]
] ]
); );