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 5657 additions and 1311 deletions

View file

@ -97,7 +97,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# 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
REFERRER_POLICY=same-origin
ENABLE_CSP=false

View file

@ -84,7 +84,11 @@ Since the release of the JSON REST API, several third-party developers have been
### 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.

View file

@ -137,23 +137,24 @@ class LdapSync extends Command
}
/* 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 ($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('Importing to ' . $location->name . ' (' . $location->id . ')');
Log::debug('Importing to '.$default_location->name.' ('.$default_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) {
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('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.');
}
@ -229,43 +230,44 @@ class LdapSync extends Command
for ($i = 0; $i < $results['count']; $i++) {
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
$item['email'] = $results[$i][$ldap_map["email"]][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
$item['email'] = $results[$i][$ldap_map["email"]][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][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
if ($ldap_map["location"] && $item['location']) {
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
}
$department = Department::firstOrCreate([
'name' => $item['department'],
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_map["location"] && $item['location']) {
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
}
$department = Department::firstOrCreate([
'name' => $item['department'],
]);
$user = User::where('username', $item['username'])->first();
if ($user) {
// Updating an existing user.
$item['createorupdate'] = 'updated';
} else {
// Creating a new user.
$user = new User;
$user->password = $user->noPassword();
$user->locale = app()->getLocale();
$user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below)
$item['createorupdate'] = 'created';
}
$user = User::where('username', $item['username'])->first();
if ($user) {
// Updating an existing user.
$item['createorupdate'] = 'updated';
} else {
// Creating a new user.
$user = new User;
$user->password = $user->noPassword();
$user->locale = app()->getLocale();
$user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below)
$item['createorupdate'] = 'created';
}
//If a sync option is not filled in on the LDAP settings don't populate the user field
if($ldap_map["username"] != null){
@ -296,7 +298,7 @@ class LdapSync extends Command
$user->department_id = $department->id;
}
if($ldap_map["location"] != null){
$user->location_id = $location ? $location->id : null;
$user->location_id = $location?->id;
}
if($ldap_map["manager"] != null){
@ -341,38 +343,38 @@ class LdapSync extends Command
}
}
// Sync activated state for Active Directory.
if ( !empty($ldap_map["active_flag"])) { // IF we have an 'active' flag set....
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_map["active_flag"]][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool)$raw_value;
// Sync activated state for Active Directory.
if (!empty($ldap_map["active_flag"])) { // IF we have an 'active' flag set....
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_map["active_flag"]][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool) $raw_value;
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
} elseif (array_key_exists('useraccountcontrol', $results[$i]) ) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
} elseif (array_key_exists('useraccountcontrol', $results[$i])) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
to allow to log in.
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
to allow to log in.
$useraccountcontrol = $results[$i]['useraccountcontrol'][0];
if(
// based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$user->activated = 0;
} */
$enabled_accounts = [
$useraccountcontrol = $results[$i]['useraccountcontrol'][0];
if(
// based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$user->activated = 0;
} */
$enabled_accounts = [
'512', // 0x200 NORMAL_ACCOUNT
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
@ -385,44 +387,47 @@ class LdapSync extends Command
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
'1114624', // 0x110200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, NOT_DELEGATED,
];
$user->activated = (in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts)) ? 1 : 0;
];
$user->activated = (in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts)) ? 1 : 0;
// If we're not using AD, and there isn't an activated flag set, activate all users
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
already-existing accounts will be however the administrator has set them */
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
already-existing accounts will be however the administrator has set them */
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
} elseif ((isset($location)) && (! empty($location))) {
if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id'];
} elseif (is_object($location)) {
$user->location_id = $location->id;
}
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
} elseif ((isset($location)) && (!empty($location))) {
if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id'];
} elseif (is_object($location)) {
$user->location_id = $location->id; //THIS is the magic line, this should do it.
}
$location = null;
$user->ldap_import = 1;
}
// 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;
$user->ldap_import = 1;
$errors = '';
$errors = '';
if ($user->save()) {
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ( $item['createorupdate'] === 'created' && $ldap_default_group) {
$user->groups()->attach($ldap_default_group);
}
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
}
$item['note'] = $errors;
$item['status'] = 'error';
if ($user->save()) {
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ($item['createorupdate'] === 'created' && $ldap_default_group) {
$user->groups()->attach($ldap_default_group);
}
array_push($summary, $item);
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
}
$item['note'] = $errors;
$item['status'] = 'error';
}
array_push($summary, $item);
}
if ($this->option('summary')) {

View file

@ -137,7 +137,6 @@ class AccessoriesController extends Controller
*/
public function store(StoreAccessoryRequest $request)
{
$this->authorize('create', Accessory::class);
$accessory = new Accessory;
$accessory->fill($request->all());
$accessory = $request->handleImages($accessory);
@ -197,9 +196,6 @@ class AccessoriesController extends Controller
$this->authorize('view', Accessory::class);
$accessory = Accessory::with('lastCheckout')->findOrFail($id);
if (! Company::isCurrentUserHasAccess($accessory)) {
return ['total' => 0, 'rows' => []];
}
$offset = request('offset', 0);
$limit = request('limit', 50);
@ -325,7 +321,7 @@ class AccessoriesController extends Controller
$accessory = Accessory::find($accessory_checkout->accessory_id);
$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?
if ($accessory_checkout->delete()) {
@ -333,14 +329,6 @@ class AccessoriesController extends Controller
$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')));
}

View file

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

View file

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

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
/**
* This controller provide the health route for
@ -15,13 +16,35 @@ use Illuminate\Routing\Controller as 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
*/
public function get()
{
return response()->json([
'status' => 'ok',
]);
try {
if (DB::select('select 2 + 2')) {
return response()->json([
'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
{
$show_user = auth()->user();
$show_users = User::where('id',auth()->user()->id)->get();
return view('users/print')
->with('assets', auth()->user()->assets)
->with('licenses', $show_user->licenses()->get())
->with('accessories', $show_user->accessories()->get())
->with('consumables', $show_user->consumables()->get())
->with('show_user', $show_user)
->with('assets', auth()->user()->assets())
->with('licenses', auth()->user()->licenses()->get())
->with('accessories', auth()->user()->accessories()->get())
->with('consumables', auth()->user()->consumables()->get())
->with('users', $show_users)
->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'));
}
$user->notify((new CurrentInventory($user)));
try {
$user->notify((new CurrentInventory($user)));
} catch (\Exception $e) {
\Log::error($e);
}
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\SettingsSamlRequest;
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\Group;
use App\Models\Setting;
@ -273,20 +278,6 @@ class SettingsController extends Controller
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.
@ -488,7 +479,7 @@ class SettingsController extends Controller
*
* @since [v1.0]
*/
public function postSecurity(Request $request) : RedirectResponse
public function postSecurity(StoreSecuritySettings $request) : RedirectResponse
{
$this->validate($request, [
'pwd_secure_complexity' => 'array',
@ -558,7 +549,7 @@ class SettingsController extends Controller
*
* @since [v1.0]
*/
public function postLocalization(Request $request) : RedirectResponse
public function postLocalization(StoreLocalizationSettings $request) : RedirectResponse
{
if (is_null($setting = Setting::getSettings())) {
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>]
* @since [v1.0]
*/
public function postAlerts(Request $request) : RedirectResponse
public function postAlerts(StoreNotificationSettings $request) : RedirectResponse
{
if (is_null($setting = Setting::getSettings())) {
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>]
* @since [v4.0]
*/
public function postLabels(Request $request) : RedirectResponse
public function postLabels(StoreLabelSettings $request) : RedirectResponse
{
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
@ -861,26 +852,7 @@ class SettingsController extends Controller
{
$setting = Setting::getSettings();
$groups = Group::pluck('name', 'id');
/**
* 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);
return view('settings.ldap', compact('setting', 'groups'));
}
/**
@ -889,7 +861,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
*/
public function postLdapSettings(Request $request) : RedirectResponse
public function postLdapSettings(StoreLdapSettings $request) : RedirectResponse
{
if (is_null($setting = Setting::getSettings())) {
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,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'health' => [
],
];
/**
@ -69,5 +73,6 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'health' => null,
];
}

View file

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

View file

@ -26,18 +26,11 @@ class StoreAssetRequest extends ImageUploadRequest
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->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
'company_id' => Company::getIdForCurrentUser($this->company_id),
'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'),
'user_id' => ($assetmaintenance->adminuser) ? [
'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
'created_by' => ($assetmaintenance->adminuser) ? [
'id' => (int) $assetmaintenance->adminuser->id,

View file

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

View file

@ -43,16 +43,16 @@ class Asset extends Depreciable
/**
* Run after the checkout acceptance was declined by the user
*
*
* @param User $acceptedBy
* @param string $signature
*/
*/
public function declinedCheckout(User $declinedBy, $signature)
{
$this->assigned_to = null;
$this->assigned_type = null;
$this->accepted = null;
$this->save();
$this->accepted = null;
$this->save();
}
/**
@ -368,7 +368,7 @@ class Asset extends Depreciable
if ($this->save()) {
if (is_int($admin)) {
$checkedOutBy = User::findOrFail($admin);
} elseif (get_class($admin) === \App\Models\User::class) {
} elseif ($admin && get_class($admin) === \App\Models\User::class) {
$checkedOutBy = $admin;
} else {
$checkedOutBy = auth()->user();
@ -1705,7 +1705,7 @@ class Asset extends Depreciable
});
});
}
/**
* THIS CLUNKY BIT IS VERY IMPORTANT
@ -1726,7 +1726,7 @@ class Asset extends Depreciable
* assets.location would fail, as that field doesn't exist -- plus we're already searching
* against those relationships earlier in this method.
*
* - snipe
* - snipe
*
*/

View file

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

View file

@ -2,10 +2,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Import extends Model
{
use HasFactory;
protected $casts = [
'header_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
* 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 = [])
{
$settings = Setting::getSettings();
$log = new Actionlog;
if($target != null){
@ -171,39 +170,6 @@ trait Loggable
$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;
}

View file

@ -51,36 +51,7 @@ class Setting extends Model
*/
protected $rules = [
'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',
'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'
];

View file

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

View file

@ -31,6 +31,7 @@ class ValidationServiceProvider extends ServiceProvider
Validator::extend('email_array', function ($attribute, $value, $parameters, $validator) {
$value = str_replace(' ', '', $value);
$array = explode(',', $value);
$email_to_validate = [];
foreach ($array as $email) { //loop over values
$email_to_validate['alert_email'][] = $email;
@ -38,7 +39,7 @@ class ValidationServiceProvider extends ServiceProvider
$rules = ['alert_email.*'=>'email'];
$messages = [
'alert_email.*'=>trans('validation.email_array'),
'alert_email.*' => trans('validation.custom.email_array'),
];
$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;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Category;
use App\Models\Location;
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 \Auth;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2,8 +2,8 @@
"/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/_all-skins.css": "/css/dist/skins/_all-skins.css?id=f4397c717b99fce41a633ca6edd5d1f4",
"/css/build/overrides.css": "/css/build/overrides.css?id=efd9f439cb0586512d03172bcd9a5752",
"/css/build/app.css": "/css/build/app.css?id=2f45befb40b9d7f038eeae9569c33a5f",
"/css/build/overrides.css": "/css/build/overrides.css?id=1c3ffc5fb379e21523f2a9b03f986edb",
"/css/build/app.css": "/css/build/app.css?id=d04f32982fb319ac35a32d362089f18b",
"/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-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-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/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.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",

View file

@ -1432,10 +1432,10 @@ var require_module_cjs = __commonJS({
});
}
function cleanupElement(el) {
if (el._x_cleanups) {
while (el._x_cleanups.length)
el._x_cleanups.pop()();
}
var _a, _b;
(_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);
while ((_b = el._x_cleanups) == null ? void 0 : _b.length)
el._x_cleanups.pop()();
}
var observer = new MutationObserver(onMutate);
var currentlyObserving = false;
@ -1673,27 +1673,23 @@ var require_module_cjs = __commonJS({
magics[name] = callback;
}
function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
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}`, {
get() {
return callback(el, getUtilities());
return callback(el, memoizedUtilities);
},
enumerable: false
});
});
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) {
try {
return callback(...args);
@ -2067,8 +2063,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
function destroyTree(root, walker = walk) {
walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el);
cleanupAttributes(el);
});
}
function warnAboutMissingPlugins() {
@ -2648,34 +2644,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return rawValue ? Boolean(rawValue) : null;
}
var booleanAttributes = /* @__PURE__ */ new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async",
"defer",
"nomodule"
];
return booleanAttributes.includes(attrName);
return booleanAttributes.has(attrName);
}
function attributeShouldntBePreservedIfFalsy(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];
}
stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init();
}
initInterceptors(stores[name]);
}
function getStores() {
return stores;
@ -3070,7 +3069,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers);
});
};
cleanup(() => clone2.remove());
cleanup(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
});
var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) {
@ -3558,7 +3560,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
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_lookup;
});
@ -3627,11 +3632,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
for (let i = 0; i < removes.length; i++) {
let key = removes[i];
if (!!lookup[key]._x_effects) {
lookup[key]._x_effects.forEach(dequeueJob);
}
lookup[key].remove();
lookup[key] = null;
if (!(key in lookup))
continue;
mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove();
});
delete lookup[key];
}
for (let i = 0; i < moves.length; i++) {
@ -3752,12 +3758,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
el._x_currentIfEl = clone2;
el._x_undoIf = () => {
walk(clone2, (node) => {
if (!!node._x_effects) {
node._x_effects.forEach(dequeueJob);
}
mutateDom(() => {
destroyTree(clone2);
clone2.remove();
});
clone2.remove();
delete el._x_currentIfEl;
};
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({
"../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 __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -3887,7 +3891,7 @@ var require_module_cjs2 = __commonJS({
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) {
if (el.getBoundingClientRect().height == full) {
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({
"../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 __defProp2 = Object.defineProperty;
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({
"../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 __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
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({
"../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 __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
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({
"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) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -6645,7 +6721,7 @@ var require_nprogress = __commonJS({
});
// ../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) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -6744,6 +6820,8 @@ var require_module_cjs7 = __commonJS({
let toAttributes = Array.from(to.attributes);
for (let i = domAttributes.length - 1; i >= 0; i--) {
let name = domAttributes[i].name;
if (name === "style")
continue;
if (!to.hasAttribute(name)) {
from2.removeAttribute(name);
}
@ -6751,6 +6829,8 @@ var require_module_cjs7 = __commonJS({
for (let i = toAttributes.length - 1; i >= 0; i--) {
let name = toAttributes[i].name;
let value = toAttributes[i].value;
if (name === "style")
continue;
if (from2.getAttribute(name) !== value) {
from2.setAttribute(name, value);
}
@ -7006,9 +7086,9 @@ var require_module_cjs7 = __commonJS({
}
});
// ../alpine/packages/mask/dist/module.cjs.js
var require_module_cjs8 = __commonJS({
"../alpine/packages/mask/dist/module.cjs.js"(exports, module) {
// ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js
var require_module_cjs9 = __commonJS({
"../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -8509,7 +8589,8 @@ var import_collapse = __toESM(require_module_cjs2());
var import_focus = __toESM(require_module_cjs3());
var import_persist2 = __toESM(require_module_cjs4());
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
var Snapshot = class {
@ -8660,7 +8741,7 @@ function extractDestinationFromLink(linkEl) {
return createUrlObjectFromString(linkEl.getAttribute("href"));
}
function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI);
return urlString !== null && new URL(urlString, document.baseURI);
}
function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash;
@ -8782,8 +8863,10 @@ function restoreScrollPositionOrScrollToTop() {
}
};
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
});
});
}
@ -9087,12 +9170,16 @@ function navigate_default(Alpine19) {
let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
});
whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
@ -9441,8 +9528,8 @@ function fromQueryString(search) {
}
// js/lifecycle.js
var import_morph = __toESM(require_module_cjs7());
var import_mask = __toESM(require_module_cjs8());
var import_morph = __toESM(require_module_cjs8());
var import_mask = __toESM(require_module_cjs9());
var import_alpinejs5 = __toESM(require_module_cjs());
function start() {
setTimeout(() => ensureLivewireScriptIsntMisplaced());
@ -9451,6 +9538,7 @@ function start() {
import_alpinejs5.default.plugin(import_morph.default);
import_alpinejs5.default.plugin(history2);
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_anchor.default);
import_alpinejs5.default.plugin(import_focus.default);

View file

@ -851,10 +851,9 @@
});
}
function cleanupElement(el) {
if (el._x_cleanups) {
while (el._x_cleanups.length)
el._x_cleanups.pop()();
}
el._x_effects?.forEach(dequeueJob);
while (el._x_cleanups?.length)
el._x_cleanups.pop()();
}
var observer = new MutationObserver(onMutate);
var currentlyObserving = false;
@ -1092,27 +1091,23 @@
magics[name] = callback;
}
function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
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}`, {
get() {
return callback(el, getUtilities());
return callback(el, memoizedUtilities);
},
enumerable: false
});
});
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) {
try {
return callback(...args);
@ -1486,8 +1481,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
function destroyTree(root, walker = walk) {
walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el);
cleanupAttributes(el);
});
}
function warnAboutMissingPlugins() {
@ -2067,34 +2062,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return rawValue ? Boolean(rawValue) : null;
}
var booleanAttributes = /* @__PURE__ */ new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async",
"defer",
"nomodule"
];
return booleanAttributes.includes(attrName);
return booleanAttributes.has(attrName);
}
function attributeShouldntBePreservedIfFalsy(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];
}
stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init();
}
initInterceptors(stores[name]);
}
function getStores() {
return stores;
@ -3136,7 +3134,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers);
});
};
cleanup2(() => clone2.remove());
cleanup2(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
});
var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) {
@ -3624,7 +3625,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
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_lookup;
});
@ -3693,11 +3697,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
for (let i = 0; i < removes.length; i++) {
let key = removes[i];
if (!!lookup[key]._x_effects) {
lookup[key]._x_effects.forEach(dequeueJob);
}
lookup[key].remove();
lookup[key] = null;
if (!(key in lookup))
continue;
mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove();
});
delete lookup[key];
}
for (let i = 0; i < moves.length; i++) {
@ -3818,12 +3823,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
el._x_currentIfEl = clone2;
el._x_undoIf = () => {
walk(clone2, (node) => {
if (!!node._x_effects) {
node._x_effects.forEach(dequeueJob);
}
mutateDom(() => {
destroyTree(clone2);
clone2.remove();
});
clone2.remove();
delete el._x_currentIfEl;
};
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) {
Alpine3.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => {
@ -4812,7 +4815,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
@ -4856,7 +4859,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
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 candidateSelector = /* @__PURE__ */ candidateSelectors.join(",");
var NoElement = typeof Element === "undefined";
@ -5805,7 +5808,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
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) {
let persist = () => {
let alias;
@ -5867,7 +5870,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
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) {
Alpine3.directive("intersect", Alpine3.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let evaluate3 = evaluateLater2(expression);
@ -5922,6 +5925,51 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
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
var min = Math.min;
var max = Math.max;
@ -7096,7 +7144,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
platform: platformWithCache
});
};
function src_default6(Alpine3) {
function src_default7(Alpine3) {
Alpine3.magic("anchor", (el) => {
if (!el._x_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");
return { placement, offsetValue, unstyled };
}
var module_default6 = src_default6;
var module_default7 = src_default7;
// js/plugins/navigate/history.js
var Snapshot = class {
@ -7305,7 +7353,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return createUrlObjectFromString(linkEl.getAttribute("href"));
}
function createUrlObjectFromString(urlString) {
return new URL(urlString, document.baseURI);
return urlString !== null && new URL(urlString, document.baseURI);
}
function getUriStringFromUrlObject(urlObject) {
return urlObject.pathname + urlObject.search + urlObject.hash;
@ -7426,8 +7474,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
};
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
});
});
}
@ -7730,12 +7780,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let shouldPrefetchOnHover = modifiers.includes("hover");
shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
});
whenThisLinkIsPressed(el, (whenItIsReleased) => {
let destination = extractDestinationFromLink(el);
if (!destination)
return;
prefetchHtml(destination, (html, finalDestination) => {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
@ -8158,6 +8212,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
let toAttributes = Array.from(to.attributes);
for (let i = domAttributes.length - 1; i >= 0; i--) {
let name = domAttributes[i].name;
if (name === "style")
continue;
if (!to.hasAttribute(name)) {
from2.removeAttribute(name);
}
@ -8165,6 +8221,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
for (let i = toAttributes.length - 1; i >= 0; i--) {
let name = toAttributes[i].name;
let value = toAttributes[i].value;
if (name === "style")
continue;
if (from2.getAttribute(name) !== value) {
from2.setAttribute(name, value);
}
@ -8413,13 +8471,13 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
to.setAttribute("id", fromId);
to.id = fromId;
}
function src_default7(Alpine3) {
function src_default8(Alpine3) {
Alpine3.morph = morph;
}
var module_default7 = src_default7;
var module_default8 = src_default8;
// ../alpine/packages/mask/dist/module.esm.js
function src_default8(Alpine3) {
// ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.esm.js
function src_default9(Alpine3) {
Alpine3.directive("mask", (el, { value, expression }, { effect: effect3, evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let templateFn = () => expression;
let lastInputValue = "";
@ -8581,22 +8639,23 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
return template;
}
var module_default8 = src_default8;
var module_default9 = src_default9;
// js/lifecycle.js
function start2() {
setTimeout(() => ensureLivewireScriptIsntMisplaced());
dispatch(document, "livewire:init");
dispatch(document, "livewire:initializing");
module_default.plugin(module_default7);
module_default.plugin(module_default8);
module_default.plugin(history2);
module_default.plugin(module_default5);
module_default.plugin(module_default2);
module_default.plugin(module_default6);
module_default.plugin(module_default2);
module_default.plugin(module_default7);
module_default.plugin(module_default3);
module_default.plugin(module_default4);
module_default.plugin(navigate_default);
module_default.plugin(module_default8);
module_default.plugin(module_default9);
module_default.addRootSelector(() => "[wire\\:id]");
module_default.onAttributesAdded((el, attributes) => {
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-right: 1px solid #dddddd;
display: table;
}
.row-striped .row:nth-of-type(odd) div {
background-color: #f9f9f9;
border-top: 1px solid #dddddd;
display: table-cell;
word-wrap: break-word;
}
.row-striped .row:nth-of-type(even) div {
background: #FFFFFF;
border-top: 1px solid #dddddd;
display: table-cell;
word-wrap: break-word;
}
.row-new-striped {
vertical-align: top;
line-height: 2.6;
padding: 0px;
margin-left: 20px;
padding: 3px;
display: table;
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) {
background: #FFFFFF;
border-top: 1px solid #dddddd;
line-height: 1.9;
display: table-row;
}
.row-new-striped > .row:nth-of-type(odd) {
background-color: #F8F8F8;
border-top: 1px solid #dddddd;
display: table-row;
line-height: 1.9;
padding: 2px;
}
.row-new-striped div {
display: table-cell;
border-top: 1px solid #dddddd;
padding: 6px;
}
.row-new-striped div {
display: table-cell;
border-top: 1px solid #dddddd;
padding: 6px;
}

View file

@ -387,5 +387,6 @@ return [
'restore_default_avatar_help' => '',
'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?',
'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.',
'uuid' => 'The :attribute field must be a valid UUID.',
/*
|--------------------------------------------------------------------------
| 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.',
// 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.
'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',
@ -206,6 +207,13 @@ return [
'checkboxes' => ':attribute contains invalid options.',
'radio_buttons' => ':attribute is invalid.',
'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') }}
</strong>
</div>
<div class="col-md-9">
<div class="col-md-9" style="word-wrap: break-word;">
{!! nl2br(Helper::parseEscapedMarkedownInline($accessory->notes)) !!}
</div>
</div>

View file

@ -103,20 +103,23 @@
</div>
@can('self.profile')
<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') }}
</a>
</div>
@endcan
<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') }}
</a>
</div>
@can('self.api')
<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') }}
</a>
</div>
@ -124,7 +127,8 @@
<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') }}
</a>
</div>
@ -134,10 +138,16 @@
@if (!empty($user->email))
<form action="{{ route('profile.email_assets') }}" method="POST">
{{ 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>
@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
</div>

View file

@ -34,6 +34,8 @@
'required' => true,
'asset_status_type' => 'RTD',
'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.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;'])
<!-- Checkout/Checkin Date -->

View file

@ -189,14 +189,14 @@ dir="{{ Helper::determineLanguageDirection() }}">
action="{{ route('findbytag/hardware') }}" method="get">
<div class="col-xs-12 col-md-12">
<div class="col-xs-12 form-group">
<label class="sr-only"
for="tagSearch">{{ trans('general.lookup_by_tag') }}</label>
<input type="text" class="form-control" id="tagSearch" name="assetTag"
placeholder="{{ trans('general.lookup_by_tag') }}">
<label class="sr-only" for="tagSearch">
{{ trans('general.lookup_by_tag') }}
</label>
<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">
</div>
<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" />
<span class="sr-only">{{ trans('general.search') }}</span>
</button>

View file

@ -25,9 +25,15 @@
<label for="currency" class="col-md-3 control-label">
{{ trans('admin/locations/table.currency') }}
</label>
<div class="col-md-9">
{{ 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 : '')) }}
{!! $errors->first('currency', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<div class="col-md-7">
<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" />
@error('currency')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
</div>
</div>
@ -40,8 +46,13 @@
{{ trans('admin/locations/table.ldap_ou') }}
</label>
<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 : '')) }}
{!! $errors->first('ldap_ou', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<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" />
@error('ldap_ou')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
</div>
</div>
@endif

View file

@ -1,5 +1,6 @@
<!-- 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')) }}
<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' : '' }}>
@ -11,6 +12,15 @@
@else
@if(!isset($multiple))
<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
</select>

View file

@ -21,9 +21,10 @@
</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_field()}}
{{ csrf_field() }}
<div class="row">
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
@ -68,12 +69,10 @@
{{ Form::label('alert_email', trans('admin/settings/general.alert_email')) }}
</div>
<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>') !!}
<p class="help-block">{{ trans('admin/settings/general.alert_email_help') }}</p>
</div>
</div>
@ -84,7 +83,7 @@
{{ Form::label('admin_cc_email', trans('admin/settings/general.admin_cc_email')) }}
</div>
<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>') !!}
<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')) }}
</div>
<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>
</div>
<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')) }}
</div>
<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>
</div>
<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')) }}
</div>
<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>
</div>
<div class="col-md-9 col-md-offset-3">
{!! $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_field()}}

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Update LDAP/AD Settings
{{ trans('admin/settings/general.ldap_ad') }}
@parent
@stop
@ -42,8 +42,7 @@
@endif
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form']) }}
<form method="POST" action="{{ route('settings.ldap.save') }}" autocomplete="off" class="form-horizontal" role="form" id="create-form">
<!-- CSRF Token -->
{{csrf_field()}}
@ -62,7 +61,7 @@
<h2 class="box-title">
<x-icon type="ldap"/>
{{ trans('admin/settings/general.ldap_ad') }}
</h4>
</h2>
</div>
<div class="box-body">
@ -76,11 +75,15 @@
<div class="col-md-8">
<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') }}
</label>
@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
</div>
</div>
@ -93,13 +96,21 @@
</div>
<div class="col-md-8">
<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') }}
</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)
<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
</div>
</div>
@ -111,14 +122,23 @@
</div>
<div class="col-md-8">
<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') }}
</label>
<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)
<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
</div>
@ -130,42 +150,43 @@
{{ Form::label('ad_domain', trans('admin/settings/general.ad_domain')) }}
</div>
<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>
{!! $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)
<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
</div>
</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 -->
<div class="form-group {{ $errors->has('ldap_client_tls_key') ? 'error' : '' }}">
<div class="col-md-3">
{{ Form::label('ldap_client_tls_key', trans('admin/settings/general.ldap_client_tls_key')) }}
</div>
<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-----
", $setting->demoMode]) }}
{!! $errors->first('ldap_client_tls_key', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ 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-----"]) }}
@error('ldap_client_tls_key')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div><!-- LDAP Client-Side TLS key -->
@ -176,11 +197,20 @@
{{ Form::label('ldap_client_tls_cert', trans('admin/settings/general.ldap_client_tls_cert')) }}
</div>
<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>
{!! $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)
<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
</div>
</div><!-- LDAP Client-Side TLS certificate -->
@ -191,11 +221,21 @@
{{ Form::label('ldap_server', trans('admin/settings/general.ldap_server')) }}
</div>
<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>
{!! $errors->first('ldap_server', '<span class="alert-msg" aria-hidden="true">: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>
<p class="text-warning">
<x-icon type="locked" />
{{ trans('general.feature_disabled') }}
</p>
@endif
</div>
</div><!-- LDAP Server -->
@ -207,12 +247,21 @@
</div>
<div class="col-md-8">
<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') }}
</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)
<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
</div>
</div>
@ -224,13 +273,24 @@
</div>
<div class="col-md-8">
<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') }}
</label>
{!! $errors->first('ldap_server_cert_ignore', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">{{ trans('admin/settings/general.ldap_server_cert_help') }}</p>
@error('ldap_server_cert_ignore')
<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)
<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
</div>
</div>
@ -241,10 +301,19 @@
{{ Form::label('ldap_uname', trans('admin/settings/general.ldap_uname')) }}
</div>
<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]) }}
{!! $errors->first('ldap_uname', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_uname', old('ldap_uname', $setting->ldap_uname), ['class' => 'form-control','autocomplete' => 'off', 'placeholder' => trans('general.example') .'binduser@example.com']) }}
@error('ldap_uname')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -255,10 +324,19 @@
{{ Form::label('ldap_pword', trans('admin/settings/general.ldap_pword')) }}
</div>
<div class="col-md-8">
{{ Form::password('ldap_pword', ['class' => 'form-control', 'autocomplete' => 'off', 'onfocus' => "this.removeAttribute('readonly');", $setting->demoMode, ' readonly']) }}
{!! $errors->first('ldap_pword', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::password('ldap_pword', ['class' => 'form-control', 'autocomplete' => 'off', 'onfocus' => "this.removeAttribute('readonly');", ' readonly']) }}
@error('ldap_pword')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -269,10 +347,19 @@
{{ Form::label('ldap_basedn', trans('admin/settings/general.ldap_basedn')) }}
</div>
<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]) }}
{!! $errors->first('ldap_basedn', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_basedn', old('ldap_basedn', $setting->ldap_basedn), ['class' => 'form-control', 'placeholder' => trans('general.example') .'cn=users/authorized,dc=example,dc=com']) }}
@error('ldap_basedn')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -283,10 +370,19 @@
{{ Form::label('ldap_filter', trans('admin/settings/general.ldap_filter')) }}
</div>
<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]) }}
{!! $errors->first('ldap_filter', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<input type="text" name="ldap_filter" id="ldap_filter" value="{{ old('ldap_filter', $setting->ldap_filter) }}" class="form-control" placeholder="{{ trans('general.example') .'&(cn=*)' }}">
@error('ldap_filter')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -297,10 +393,19 @@
{{ Form::label('ldap_username_field', trans('admin/settings/general.ldap_username_field')) }}
</div>
<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]) }}
{!! $errors->first('ldap_username_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<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' }}">
@error('ldap_username_field')
<span class="alert-msg">
<x-icon type="x" />
{!! $message !!}
</span>
@enderror
@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
</div>
</div>
@ -311,10 +416,19 @@
{{ Form::label('ldap_lname_field', trans('admin/settings/general.ldap_lname_field')) }}
</div>
<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]) }}
{!! $errors->first('ldap_lname_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<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' }}">
@error('ldap_lname_field')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -325,10 +439,19 @@
{{ Form::label('ldap_fname_field', trans('admin/settings/general.ldap_fname_field')) }}
</div>
<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]) }}
{!! $errors->first('ldap_fname_field', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<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' }}">
@error('ldap_fname_field')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -336,13 +459,23 @@
<!-- LDAP Auth Filter Query -->
<div class="form-group {{ $errors->has('ldap_auth_filter_query') ? 'error' : '' }}">
<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 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)
<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
</div>
</div>
@ -364,7 +497,6 @@
@endforeach
</ul>
<span class="help-block">{{ trans('admin/users/general.group_memberships_helpblock') }}</span>
@else
<div class="controls">
@ -383,7 +515,7 @@
</div>
@endif
@else
<p>No groups have been created yet. Visit <code>Admin Settings > Permission Groups</code> to add one.</p>
<p>{!! trans('admin/settings/general.no_groups') !!}</p>
@endif
</div>
@ -395,13 +527,21 @@
{{ Form::label('ldap_active_flag', trans('admin/settings/general.ldap_active_flag')) }}
</div>
<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>
{!! $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)
<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
</div>
</div>
@ -412,10 +552,19 @@
{{ Form::label('ldap_emp_num', trans('admin/settings/general.ldap_emp_num')) }}
</div>
<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]) }}
{!! $errors->first('ldap_emp_num', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_emp_num', old('ldap_emp_num', $setting->ldap_emp_num), ['class' => 'form-control','placeholder' => trans('general.example') .'employeenumber/employeeid']) }}
@error('ldap_emp_num')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -425,10 +574,20 @@
{{ Form::label('ldap_dept', trans('admin/settings/general.ldap_dept')) }}
</div>
<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]) }}
{!! $errors->first('ldap_dept', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_dept', old('ldap_dept', $setting->ldap_dept), ['class' => 'form-control','placeholder' => trans('general.example') .'department']) }}
@error('ldap_dept')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -438,10 +597,19 @@
{{ Form::label('ldap_dept', trans('admin/settings/general.ldap_manager')) }}
</div>
<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]) }}
{!! $errors->first('ldap_manager', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_manager', old('ldap_manager', $setting->ldap_manager), ['class' => 'form-control','placeholder' => trans('general.example') .'manager']) }}
@error('ldap_manager')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -452,10 +620,19 @@
{{ Form::label('ldap_email', trans('admin/settings/general.ldap_email')) }}
</div>
<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]) }}
{!! $errors->first('ldap_email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_email', old('ldap_email', $setting->ldap_email), ['class' => 'form-control','placeholder' => trans('general.example') .'mail']) }}
@error('ldap_email')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -466,10 +643,19 @@
{{ Form::label('ldap_phone', trans('admin/settings/general.ldap_phone')) }}
</div>
<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]) }}
{!! $errors->first('ldap_phone', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_phone', old('ldap_phone', $setting->ldap_phone_field), ['class' => 'form-control','placeholder' => trans('general.example') .'telephonenumber']) }}
@error('ldap_phone')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -480,10 +666,19 @@
{{ Form::label('ldap_jobtitle', trans('admin/settings/general.ldap_jobtitle')) }}
</div>
<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]) }}
{!! $errors->first('ldap_jobtitle', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_jobtitle', old('ldap_jobtitle', $setting->ldap_jobtitle), ['class' => 'form-control','placeholder' => trans('general.example') .'title']) }}
@error('ldap_jobtitle')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -494,10 +689,19 @@
{{ Form::label('ldap_country', trans('admin/settings/general.ldap_country')) }}
</div>
<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]) }}
{!! $errors->first('ldap_country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{{ Form::text('ldap_country', old('ldap_country', $setting->ldap_country), ['class' => 'form-control','placeholder' => trans('general.example') .'c']) }}
@error('ldap_country')
<span class="alert-msg">
<x-icon type="x" />
{{ $message }}
</span>
@enderror
@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
</div>
</div>
@ -507,11 +711,20 @@
{{ Form::label('ldap_location', trans('admin/settings/general.ldap_location')) }}
</div>
<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>
{!! $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)
<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
</div>
</div>
@ -523,7 +736,7 @@
{{ Form::label('test_ldap_sync', 'Test LDAP Sync') }}
</div>
<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 class="col-md-8 col-md-offset-3">
<br />
@ -532,7 +745,10 @@
<div class="col-md-8 col-md-offset-3">
<p class="help-block">{{ trans('admin/settings/general.ldap_login_sync_help') }}</p>
@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
</div>
@ -578,11 +794,20 @@
{{ Form::label('custom_forgot_pass_url', trans('admin/settings/general.custom_forgot_pass_url')) }}
</div>
<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>
{!! $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)
<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
</div>
</div><!-- LDAP Server -->
@ -607,9 +832,10 @@
{{Form::close()}}
@stop
@endsection
@push('js')
<script nonce="{{ csrf_token() }}">
@ -618,11 +844,58 @@
* Check to see if is_ad is checked, if not disable the ad_domain field
*/
$(function() {
if( $('#is_ad').prop('checked') === false) {
// 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) {
$('#ad_domain').prop('disabled', 'disabled');
} else {
//$('#ldap_server').prop('disabled', 'disabled');
$("#ad_domain").prop('required',false);
}
// 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() {
@ -649,7 +922,7 @@
$("#ldaptest").click(function () {
$("#ldapad_test_results").removeClass('hidden text-success text-danger');
$("#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({
url: '{{ route('api.settings.ldaptest') }}',
type: 'GET',
@ -698,8 +971,8 @@
*/
function buildLdapTestResults(results) {
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" aria-hidden="true"></i> ' + results.bind.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""></i> ' + results.bind.message + ' </li>'
html += '</ul>'
html += '<div style="overflow:auto;">'
html += '<div>{{ trans('admin/settings/message.ldap.sync_success') }}</div>'
@ -738,12 +1011,13 @@
return body;
}
$("#ldaptestlogin").click(function(){
$("#ldaptestloginrow").removeClass('text-success');
$("#ldaptestloginrow").removeClass('text-danger');
$("#ldaptestloginstatus").removeClass('text-danger');
$("#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({
url: '{{ route('api.settings.ldaptestlogin') }}',
type: 'POST',
@ -803,9 +1077,6 @@
}
}
});
});
</script>

View file

@ -16,9 +16,10 @@
{{ 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_field()}}
{{ csrf_field() }}
<div class="row">
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">

View file

@ -2,7 +2,7 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<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>
@else
<title>{{ trans('admin/users/general.print_assigned') }} - {{ date('Y-m-d H:i', time()) }}</title>
@ -36,11 +36,7 @@
@page {
size: A4;
}
#start_of_user_section {
break-before: page;
}
.print-logo {
max-height: 40px;
}
@ -51,13 +47,6 @@
}
</style>
<script nonce="{{ csrf_token() }}">
window.snipeit = {
settings: {
"per_page": 50
}
};
</script>
</head>
<body>
@ -384,8 +373,11 @@
</table>
@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... --}}
@if (count($users) === 1)
@if (count($users) === 1 && !empty($eulas))
<p></p>
<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">
@ -395,19 +387,16 @@
@endif
<table style="margin-top: 80px;">
@if (!empty($eulas))
<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; padding-bottom: 80px;" colspan="3">
@php
if (!empty($eulas)) $eulas = array_unique($eulas);
@endphp
@if (!empty($eulas))
@foreach ($eulas as $key => $eula)
{!! $eula !!}
@endforeach
@endif
@foreach ($eulas as $key => $eula)
{!! $eula !!}
@endforeach
</td>
</tr>
@endif
<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;">______________________________________</td>

View file

@ -31,7 +31,7 @@
<x-icon type="assets" class="fa-2x" />
</span>
<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>
</a>
</li>
@ -177,8 +177,6 @@
<div class="col-md-12 text-center">
<img src="{{ $user->present()->gravatar() }}" class=" img-thumbnail hidden-print" style="margin-bottom: 20px;" alt="{{ $user->present()->fullName() }}">
</div>
@can('update', $user)
<div class="col-md-12">

View file

@ -536,13 +536,16 @@ Route::group(['middleware' => 'web'], function () {
)->name('logout.post');
});
//Auth::routes();
Route::get(
'/health',
/**
* Health check route - skip middleware
*/
Route::withoutMiddleware(['web'])->get(
'/health',
[HealthController::class, 'get']
)->name('health');
Route::middleware(['auth'])->get(
'/',
[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;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
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())
->getJson(route('api.accessories.index'))
->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;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class ShowAccessoryTest extends TestCase
class ShowAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testPermissionRequiredToShowAccessory()
public function testRequiresPermission()
{
$accessory = Accessory::factory()->create();
@ -16,4 +19,43 @@ class ShowAccessoryTest extends TestCase
->getJson(route('api.accessories.show', $accessory))
->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;
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 Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
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())
->postJson(route('api.accessories.store'))
->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;
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 Tests\Concerns\TestsFullMultipleCompaniesSupport;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class UpdateAccessoryTest extends TestCase
class UpdateAccessoryTest extends TestCase implements TestsFullMultipleCompaniesSupport, TestsPermissionsRequirement
{
public function testPermissionRequiredToUpdateAccessory()
public function testRequiresPermission()
{
$accessory = Accessory::factory()->create();
@ -16,4 +23,84 @@ class UpdateAccessoryTest extends TestCase
->patchJson(route('api.accessories.update', $accessory))
->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\Notifications\CheckoutAccessoryNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class AccessoryCheckoutTest extends TestCase
class AccessoryCheckoutTest extends TestCase implements TestsPermissionsRequirement
{
public function testCheckingOutAccessoryRequiresCorrectPermission()
public function testRequiresPermission()
{
$this->actingAsForApi(User::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;
use App\Models\Asset;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
use App\Models\Setting;
class AlertsSettingTest extends TestCase
{
public function testPermissionRequiredToViewAlertSettings()
{
$asset = Asset::factory()->create();
$this->actingAs(User::factory()->create())
->get(route('settings.alerts.index'))
->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;
use App\Models\Asset;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
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;
}
);
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,
0 => [
'sn' => 'Surname',
'firstName' => 'FirstName'
'firstname' => 'FirstName'
]
]
);