Compare commits

..

111 Commits

Author SHA1 Message Date
snipe 37381f6976 @!&(&*^&%&!!!
Signed-off-by: snipe <snipe@snipe.net>
2024-10-15 16:39:09 +01:00
snipe 2ad891cc9c Beginning of drag+drop re-ordering for status labels
Signed-off-by: snipe <snipe@snipe.net>
2024-10-15 14:29:50 +01:00
snipe 914a647204 Merge pull request #15669 from snipe/remlove_ou_requirenedess
Fixed #15663 - remove requiredness for OU
2024-10-15 12:47:15 +01:00
snipe e9225ff3ea Switch to regular HTML for input form field
Signed-off-by: snipe <snipe@snipe.net>
2024-10-15 12:43:10 +01:00
snipe d0d4159088 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2024-10-15 12:42:09 +01:00
snipe 69b6080bd8 Merge pull request #15666 from snipe/updated_readme_llm
Added LLM note
2024-10-15 10:31:29 +01:00
snipe b997d728fb Added LLM note
Signed-off-by: snipe <snipe@snipe.net>
2024-10-15 10:30:34 +01:00
snipe ddead359d0 Merge pull request #15660 from Toreg87/fixes/api_asset_create_fmcs2
Refactor asset creation with API
2024-10-14 14:29:34 +01:00
Tobias Regnery f3c4e55667 Refactor asset creation with API
Commit fb4fe3004 restored the previous behaviour to check the company_id in case of FullMultipleCompanySupport.
But after rereading the code and the laravel documentation, the check is already there where it belongs in AssetStoreRequest::prepareForValidation()
The bug is the is_int-check of the request input in prepareForValidation(). Is is of type string even if it is a numeric value, so the call to getIdForCurrentUser() never happend.
Fix this by removing the check and the now redundant call to getIdForCurrentUser().
Wrong values will get caught by the model-level validation rules.
2024-10-14 15:14:41 +02:00
snipe 0d35335da7 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2024-10-11 16:06:17 +01:00
snipe feaa714304 Nicer disabled button
Signed-off-by: snipe <snipe@snipe.net>
2024-10-11 14:23:57 +01:00
snipe e1a70023b1 Merge pull request #15655 from Toreg87/fixes/api_asset_create_fmcs
Fixes #15654 Fix asset creation with API and FullMultipleCompanySupport
2024-10-11 11:45:48 +01:00
snipe de62359c67 Merge pull request #15533 from marcusmoore/testing/fmcs-accessories
Added tests for accessory api controller
2024-10-11 11:29:34 +01:00
snipe 12bda8fc7b Merge pull request #15653 from snipe/15651_admin_user_on_maintenances
Fixed #15651 - admin user now displaying on maintenances page
2024-10-11 11:27:18 +01:00
Tobias Regnery fb4fe30049 Fix asset creation with API and FullMultipleCompanySupport
It is currently possible to create an asset with arbitrary company without being superuser and FullMultipleCompanySupport enabled.
This bug goes back to 75ac7f80b9 which is part of version 6.3.0.
Fix this by restoring the previous behaviour to check the company_id with getIdForCurrentUser().
2024-10-11 12:19:20 +02:00
snipe b054017c9f Fixed #15651 - admin user now displaying on maintenances page
Signed-off-by: snipe <snipe@snipe.net>
2024-10-11 11:16:24 +01:00
snipe 8aa298f6b0 Merge pull request #15644 from snipe/form_requests_for_settings
Form requests for settings
2024-10-10 12:30:53 +01:00
snipe 1f34657734 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2024-10-10 12:30:35 +01:00
snipe 0856ee648e Merge pull request #15648 from snipe/update_packages
Updated livewire to 3.5.2
2024-10-10 12:23:14 +01:00
snipe 1dafc970df Updated livewire to 3.5.2
Signed-off-by: snipe <snipe@snipe.net>
2024-10-10 12:19:57 +01:00
snipe 94a074a193 Merge pull request #15601 from snipe/check_db_on_healthcheck
Fixed #15439 - check database on healthcheck
2024-10-10 01:11:10 +01:00
snipe 2d49e1eff2 Merge pull request #15637 from akemidx/bug/sc-26614
FIXED: Badge counter showing deleted assets on User page
2024-10-10 01:09:11 +01:00
snipe 705bc6f0c0 Merge pull request #15642 from uberbrady/fix_bulk_checkout
Fix bulk checkout to users, assets, and locations
2024-10-10 01:08:25 +01:00
snipe b5b93fdd3a Make ldap username required
Signed-off-by: snipe <snipe@snipe.net>
2024-10-10 00:27:00 +01:00
snipe d9432baf7a Mlore ldap style improvements
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 23:51:20 +01:00
snipe 90be2a4498 Use newer naming convention for errors
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:26:30 +01:00
snipe 3886da8941 Remove form request from get LDAP method
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:15:49 +01:00
snipe 130e0c6242 More validation
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:15:37 +01:00
snipe 4361a10818 Added string
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:15:30 +01:00
snipe aa8048ac15 Blade changes for ldap
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:15:25 +01:00
snipe 710e738e8e Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 22:15:16 +01:00
snipe 3705b91439 Added more validation
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 20:51:34 +01:00
snipe 707bdad192 Updated test
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 20:33:56 +01:00
snipe 242fe33f97 Switch to regular HTML input
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 20:33:42 +01:00
snipe ded79469c1 Remove unused controller method
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 20:33:29 +01:00
snipe d9fbf330e5 Fixed translations
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 20:33:15 +01:00
snipe 2cb9ac26cd Renamed test
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:57:04 +01:00
snipe 185bc966e6 Cleaned up use statements in tests
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:46:47 +01:00
snipe a7f7e4938f Added form action
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:31:56 +01:00
snipe 2883e79193 Removed unecessary assets creation
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:30:55 +01:00
snipe 9c4191ae0a Basic tests
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:30:42 +01:00
snipe 3a77b83e9c Added space
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:30:34 +01:00
snipe d9be2b5a5e Trying to use the email_array translation
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:30:25 +01:00
snipe 69c43c610c Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 19:30:00 +01:00
snipe 4f957bcf71 Required flag
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 18:34:53 +01:00
snipe 5cda7cce48 Only accept a positive number for thresholds
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 18:21:40 +01:00
snipe 41b94e7128 Fixed form input group
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 18:19:26 +01:00
snipe aa55fa6ff4 Switch to form requests for settings
Signed-off-by: snipe <snipe@snipe.net>
2024-10-09 18:16:34 +01:00
Brady Wetherington 67a605c9a5 Fix bulk checkout to users, assets, and locations 2024-10-09 17:01:26 +01:00
akemidx c2663ea1e0 withouttrashed 2024-10-08 16:38:33 -04:00
snipe 3ee5713740 Merge pull request #15631 from snipe/test/importer-tests
Add importer tests
2024-10-07 23:13:15 +01:00
snipe ab8a22f77e Merge pull request #15630 from marcusmoore/bug/sc-27028
Only show EULA when available on print users page
2024-10-07 22:56:23 +01:00
snipe 56e7ea6677 Merge pull request #15616 from marcusmoore/test/importer-test-updates
Improve importer tests
2024-10-07 22:29:35 +01:00
snipe 26d7572bcc Merge pull request #15603 from marcusmoore/fixes/add-reguard
Added `Model::reguard()` to importer
2024-10-07 22:28:53 +01:00
Marcus Moore 8c9132aff9 Hide EULA text and button when nothing will be displayed 2024-10-07 14:22:49 -07:00
snipe 382ebef8ca Merge pull request #15621 from sniff122/develop
Docker Env: Change trusted proxies to RFC1918
2024-10-07 21:26:21 +01:00
snipe 2be88cb955 Merge pull request #15624 from snipe/fixed_line_break_on_print_all_for_users
Removed duplicate JS and removed line break before user section
2024-10-07 11:14:24 +01:00
snipe 3f36d5f9b3 Removed duplicate CSS and removed line break before user section
Signed-off-by: snipe <snipe@snipe.net>
2024-10-07 11:08:02 +01:00
Lewis Foster f76da48448 Docker Env: Change trusted proxies to RFC1918 2024-10-05 18:27:42 +01:00
Marcus Moore 8035326675 Add test 2024-10-03 16:53:19 -07:00
Marcus Moore dfdd85abb1 Remove unused imports 2024-10-03 15:14:07 -07:00
Marcus Moore 063ea1892b Add trait to clean up files after test runs 2024-10-03 15:02:03 -07:00
Marcus Moore e213053775 Swap factory syntax 2024-10-03 13:59:58 -07:00
Marcus Moore 88d549e7c5 Remove unused data provider method 2024-10-03 13:40:37 -07:00
Marcus Moore 983a25aa5f Simplify permission tests 2024-10-03 13:39:54 -07:00
Marcus Moore bde05d6ed9 Use new() instead of times() 2024-10-03 13:15:49 -07:00
Marcus Moore b5ffe54bd0 Swap assertEquals parameter order 2024-10-03 13:15:02 -07:00
Marcus Moore 863c0a8b60 Fix import_type for accessory state 2024-10-03 11:43:02 -07:00
Marcus Moore 32551d55d7 Merge pull request #15579 from bryanlopezinc/ImportTests
Add Import data tests
2024-10-03 11:39:21 -07:00
snipe 7fc498a597 Merge pull request #15613 from snipe/css_fixes_for_long_values
Smarter word-wrapping on long text
2024-10-03 17:39:18 +01:00
snipe cb281c6408 Tweaked line height
Signed-off-by: snipe <snipe@snipe.net>
2024-10-03 17:33:46 +01:00
snipe f483cd448f Smarter work-wrapping on long text
Signed-off-by: snipe <snipe@snipe.net>
2024-10-03 17:27:37 +01:00
snipe 78ca1026fd Merge pull request #15612 from snipe/fixes_print_assigned_in_profile
Fixes print assigned in profile
2024-10-03 16:24:32 +01:00
snipe 722d5a58e7 Added isset on users
Signed-off-by: snipe <snipe@snipe.net>
2024-10-03 16:19:38 +01:00
snipe 7461c3e0ca Change controller to assume a collection. (This is dumb, but whatever)
Signed-off-by: snipe <snipe@snipe.net>
2024-10-03 16:19:27 +01:00
snipe 3c0f4181ae Use the newer button style
Signed-off-by: snipe <snipe@snipe.net>
2024-10-03 16:19:06 +01:00
snipe 3699d79363 Merge pull request #15610 from uberbrady/fix_numeric_sort_bug
Fix numeric sort 'ambiguous order clause' error
2024-10-03 14:34:00 +01:00
Brady Wetherington 350b627ce1 Fix numeric sort 'ambiguous order clause' error 2024-10-03 14:23:40 +01:00
Marcus Moore ee046a8688 Add matching Model::reguard() 2024-10-02 10:50:40 -07:00
snipe b34a7c8aad Removed die()
Signed-off-by: snipe <snipe@snipe.net>
2024-10-02 15:48:35 +01:00
snipe f92bf5dc20 Updated language for failure
Signed-off-by: snipe <snipe@snipe.net>
2024-10-02 15:39:27 +01:00
snipe 4d9e85026a Fixed #15439 - check database on healthcheck
Signed-off-by: snipe <snipe@snipe.net>
2024-10-02 15:36:01 +01:00
Marcus Moore 3dc64cc5e0 Reference accessory checkout and not the accessory 2024-10-01 13:35:39 -07:00
Marcus Moore 5b90d79494 Use created_by 2024-10-01 11:50:48 -07:00
Marcus Moore fdfea390fb Merge branch 'develop' into testing/fmcs-accessories 2024-09-30 12:59:27 -07:00
bryanlopezinc e807cfab86 Merge branch 'develop' into importTests 2024-09-30 12:47:52 +01:00
bryanlopezinc 0b3f458561 Added tests for Import feature 2024-09-30 12:42:41 +01:00
Marcus Moore d609ed50a4 Merge branch 'develop' into testing/fmcs-accessories 2024-09-19 13:20:55 -07:00
Marcus Moore d639d6fbc1 Add tests for accessory select list endpoint 2024-09-19 12:21:54 -07:00
Marcus Moore fff069824b Add tests for update accessory endpoint 2024-09-19 12:11:03 -07:00
Marcus Moore a5be18bb14 Add test for limit and offset 2024-09-19 11:55:15 -07:00
Marcus Moore 2137890496 Remove dead code 2024-09-19 11:33:20 -07:00
Marcus Moore 9b22d6d493 Add tests for accessory checkouts endpoint 2024-09-19 11:24:01 -07:00
Marcus Moore 86f13a9735 Add index test 2024-09-18 16:13:34 -07:00
Marcus Moore a071fff954 Implement tests 2024-09-18 12:32:14 -07:00
Marcus Moore 8b50ef077d Implement test 2024-09-18 11:58:33 -07:00
Marcus Moore 607f29030f Stub out test 2024-09-18 11:53:27 -07:00
Marcus Moore a6bcd3c0c2 Add validation test 2024-09-18 11:47:59 -07:00
Marcus Moore 9b293afaac Remove more dead code 2024-09-18 11:28:29 -07:00
Marcus Moore 636c776620 Remove dead code 2024-09-18 11:27:37 -07:00
Marcus Moore d5f659024c Add test for logging 2024-09-18 11:27:09 -07:00
Marcus Moore 832e50a71e Implement test 2024-09-18 11:19:41 -07:00
Marcus Moore fa19686248 Implement test 2024-09-18 11:18:21 -07:00
Marcus Moore c021609c13 Merge branch 'develop' into testing/fmcs-accessories 2024-09-18 10:42:58 -07:00
Marcus Moore 7b31df7c14 Begin additional test case 2024-09-17 09:11:58 -07:00
Marcus Moore 12fd9cd97a Update interface name 2024-09-16 17:00:08 -07:00
Marcus Moore 9e03c4ba6f Merge branch 'testing/fmcs' into testing/fmcs-accessories 2024-09-16 16:58:46 -07:00
Marcus Moore 8eb1c487bc implement FMCS test for accessory index 2024-09-16 16:52:10 -07:00
Marcus Moore e835637ef0 Add accessory checkin test 2024-09-16 16:40:10 -07:00
Marcus Moore eb35608bb5 Remove call handled by form request 2024-09-16 16:35:33 -07:00
Marcus Moore 3f07f682de Migrate existing tests to TestsPermissionsRequirement 2024-09-16 16:35:21 -07:00
100 changed files with 8016 additions and 1356 deletions
+1 -1
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
+5 -1
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.
@@ -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')));
}
@@ -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);
}
@@ -65,7 +65,7 @@ class ImportController extends Controller
ini_set('auto_detect_line_endings', '1');
}
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
$reader->setDelimiter(request('delimiter', ','));
try {
$import->header_row = $reader->fetchOne(0);
} catch (JsonEncodingException $e) {
@@ -133,7 +133,6 @@ class ImportController extends Controller
}
$import->filesize = filesize($path.'/'.$file_name);
$import->delimiter = request('delimiter');
$import->save();
$results[] = $import;
+26 -3
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);
}
}
}
+12 -7
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'));
}
}
+11 -39
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.
@@ -486,7 +477,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',
@@ -556,7 +547,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'));
@@ -599,7 +590,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'));
@@ -780,7 +771,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'));
@@ -859,26 +850,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'));
}
/**
@@ -887,7 +859,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'));
+5
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,
];
}
+9 -4
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');
}
+1 -8
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,
]);
}
+41
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',
];
}
}
+38
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',
];
}
}
@@ -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',
];
}
}
@@ -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',
];
}
}
@@ -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',
];
}
}
@@ -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,
@@ -23,6 +23,7 @@ class StatuslabelsTransformer
{
$array = [
'id' => (int) $statuslabel->id,
'reorder_icon' => '<i class="fas fa-ellipsis-v"></i>',
'name' => e($statuslabel->name),
'type' => $statuslabel->getStatuslabelType(),
'color' => ($statuslabel->color) ? e($statuslabel->color) : null,
+1 -2
View File
@@ -129,10 +129,8 @@ abstract class Importer
// However, for testing we also support passing a string directly
if (is_file($file)) {
$this->csv = Reader::createFromPath($file);
$this->csv->setDelimiter($this->delimiter);
} else {
$this->csv = Reader::createFromString($file);
$this->csv->setDelimiter($this->delimiter);
}
$this->tempPassword = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40);
}
@@ -166,6 +164,7 @@ abstract class Importer
$this->log('------------- Action Summary ----------------');
}
Model::reguard();
});
}
+1 -3
View File
@@ -40,7 +40,6 @@ class Importer extends Component
public $consumables_fields;
public $components_fields;
public $aliases_fields;
public $delimiter;
protected $rules = [
'files.*.file_path' => 'required|string',
@@ -48,8 +47,7 @@ class Importer extends Component
'files.*.filesize' => 'required|integer',
'headerRow' => 'array',
'typeOfImport' => 'string',
'field_map' => 'array',
'delimiter' => 'in:comma,semicolon,pipe',
'field_map' => 'array'
];
/**
+7 -7
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
*
*/
+1 -1
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();
}
+3 -33
View File
@@ -2,46 +2,16 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
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',
'field_map' => 'json',
];
protected function delimiter(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value == 'semicolon') {
return ';';
}
if ($value == 'pipe') {
return '|';
}
return ',';
},
get: function ($value) {
if ($value == 'semicolon') {
return ';';
}
if ($value == 'pipe') {
return '|';
}
return ',';
}
);
}
}
+1 -1
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.
*
-34
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;
}
-29
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'
];
@@ -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,
+8
View File
@@ -21,6 +21,14 @@ class StatusLabelPresenter extends Presenter
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
],
[
'field' => 'reorder_icon',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('admin/custom_fields/general.reorder'),
'visible' => true,
], [
'field' => 'name',
'searchable' => true,
+2 -1
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);
Generated
+616 -681
View File
File diff suppressed because it is too large Load Diff
+15 -1
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,
]);
}
});
}
}
+146
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;
});
}
}
+3
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
{
/**
@@ -11,8 +11,8 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('imports', function (Blueprint $table) {
$table->string('delimiter', 1)->nullable()->default(',');
Schema::table('status_labels', function (Blueprint $table) {
$table->tinyInteger('display_order')->nullable()->default(0);
});
}
@@ -21,8 +21,8 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('imports', function (Blueprint $table) {
$table->dropColumn('delimiter');
Schema::table('status_labels', function (Blueprint $table) {
$table->dropColumn('display_order');
});
}
};
+6
View File
@@ -35,6 +35,7 @@
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"signature_pad": "^4.2.0",
"tablednd": "^1.0.5",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.94.0"
@@ -10388,6 +10389,11 @@
"acorn-node": "^1.2.0"
}
},
"node_modules/tablednd": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tablednd/-/tablednd-1.0.5.tgz",
"integrity": "sha512-iC230jusaekjzzAy0jigZLbzb7jKKnW9xiNiZ4W17p7y8AvBYf5KunyrIE3HwL1FhL2VEeogZtjP5VeBeIW4xg=="
},
"node_modules/tableexport.jquery.plugin": {
"version": "1.30.0",
"license": "MIT",
+1
View File
@@ -55,6 +55,7 @@
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"signature_pad": "^4.2.0",
"tablednd": "^1.0.5",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.94.0"
+10 -4
View File
@@ -942,20 +942,21 @@ h4 {
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;
}
/**
* NEW STRIPING
@@ -965,20 +966,25 @@ 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;
}
.row-new-striped div[class^="col"]:first-child {
font-weight: bold;
+10 -4
View File
@@ -574,20 +574,21 @@ h4 {
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;
}
/**
* NEW STRIPING
@@ -597,20 +598,25 @@ 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;
}
.row-new-striped div[class^="col"]:first-child {
font-weight: bold;
+20 -8
View File
@@ -21914,20 +21914,21 @@ h4 {
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;
}
/**
* NEW STRIPING
@@ -21937,20 +21938,25 @@ 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;
}
.row-new-striped div[class^="col"]:first-child {
font-weight: bold;
@@ -23389,20 +23395,21 @@ h4 {
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;
}
/**
* NEW STRIPING
@@ -23412,20 +23419,25 @@ 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;
}
.row-new-striped div[class^="col"]:first-child {
font-weight: bold;
+2
View File
@@ -2,6 +2,8 @@
.fix-sticky{position:fixed !important;overflow:hidden;z-index:100}.fix-sticky table thead{background:#fff}.fix-sticky table thead.thead-light{background:#e9ecef}.fix-sticky table thead.thead-dark{background:#212529}
.reorder-rows-on-drag-class td{background-color:#eee;box-shadow:6px 4px 5px 1px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-box-shadow:6px 4px 5px 1px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset}.reorder-rows-on-drag-class td:last-child{box-shadow:0 9px 4px -4px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset,-1px 0 0 #ccc inset;-box-shadow:0 9px 4px -4px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset,-1px 0 0 #ccc inset}
/*
* dragtable
*
+2316
View File
File diff suppressed because it is too large Load Diff
+5 -5
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",
@@ -108,8 +108,8 @@
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=393aaa7b368b0670fc42434c8cca7dc7",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=393d720a0f9aba560094fbc8d3b0c0f0",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=9a35fafd5eb2a66d108b8b669e84f9e1",
"/js/build/vendor.js": "/js/build/vendor.js?id=5269eb5a6beb74f03387c78938cf17b2",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=6660df122e24940d42d03c06775fec7b",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=bdb65484f5b26ceb5d8ca13331ff2749",
"/js/dist/all.js": "/js/dist/all.js?id=e0a4b1a80b09333a460973137f39eab4"
}
+165 -77
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);
+134 -75
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
+1 -1
View File
@@ -1,2 +1,2 @@
{"/livewire.js":"87e1046f"}
{"/livewire.js":"923613aa"}
+10 -8
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;
}
@@ -385,5 +385,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.',
];
+9 -1
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.'],
],
/*
|--------------------------------------------------------------------------
+1 -1
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>
+16 -6
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>
@@ -47,7 +47,6 @@
<!-- drag handle -->
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
</td>
@endcan
@@ -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 -->
+5 -5
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>
+34 -77
View File
@@ -26,10 +26,7 @@
<div class="box-body">
<div class="alert alert-warning">
<i class="fa fa-warning info" aria-hidden="true"></i>
<strong>
{{ trans('general.warning', ['warning'=> trans('general.errors_importing')]) }}
</strong>
<i class="fa fa-warning info" aria-hidden="true"></i> <strong>{{ trans('general.warning', ['warning'=> trans('general.errors_importing')]) }}</strong>
</div>
<div class="errors-table">
@@ -62,19 +59,41 @@
</div>
@endif
<div class="col-md-8">
<div class="col-md-9">
<div class="box">
<div class="box-body">
<div class="row">
@if($progress != -1)
<div class="col-md-12" style="height: 35px;" id='progress-container'>
<div class="progress progress-striped-active" style="height: 100%;">
<div id='progress-bar' class="progress-bar {{ $progress_bar_class }}" role="progressbar" style="width: {{ $progress }}%">
<h4 id="progress-text">{!! $progress_message !!}</h4>
<div class="col-md-12">
@if($progress != -1)
<div class="col-md-10 col-sm-5 col-xs-12" style="height: 35px;" id='progress-container'>
<div class="progress progress-striped-active" style="height: 100%;">
<div id='progress-bar' class="progress-bar {{ $progress_bar_class }}" role="progressbar" style="width: {{ $progress }}%">
<h4 id="progress-text">{!! $progress_message !!}</h4>
</div>
</div>
</div>
@endif
<div class="col-md-4 col-sm-5 col-xs-12 text-right pull-right">
<!-- The fileinput-button span is used to style the file input field as button -->
@if (!config('app.lock_passwords'))
<span class="btn btn-primary fileinput-button">
<span>{{ trans('button.select_file') }}</span>
<!-- The file input field used as target for the file upload widget -->
<label for="files[]"><span class="sr-only">{{ trans('admin/importer/general.select_file') }}</span></label>
<input id="fileupload" type="file" name="files[]" data-url="{{ route('api.imports.index') }}" accept="text/csv" aria-label="files[]">
</span>
@endif
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-md-12 table-responsive" style="padding-top: 30px;">
@@ -269,75 +288,13 @@
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">
<x-icon type="import"/>
{{ trans('general.importing') }}
</h2>
<div class="box-tools pull-right">
</div>
</div><!-- /.box-header -->
<div class="box-body">
<p>{!! trans('general.importing_help') !!}</p>
@if (config('app.lock_passwords')===true)
<p class="alert alert-warning">
<i class="fas fa-lock">
</i>
{{ trans('general.feature_disabled') }}
</p>
@else
<form class="form-horizontal">
<fieldset>
<legend style="border: 0px; font-size: 15px;">Select Delimiter:</legend>
<label class="form-control">
<input type="radio" name="delimiter" id="delimiter" value="comma" wire:model.live="delimiter">
Comma
</label>
<label class="form-control">
<input type="radio" name="delimiter" id="delimiter" value="semicolon" wire:model.live="delimiter">
Semicolon
</label>
<label class="form-control">
<input type="radio" name="delimiter" id="delimiter" value="pipe" wire:model.live="delimiter">
Pipe
</label>
</fieldset>
<span class="btn btn-primary fileinput-button">
<span>
<x-icon type="paperclip" />
{{ trans('button.select_file') }}
</span>
<!-- The file input field used as target for the file upload widget -->
<label for="files[]">
<span class="sr-only">{{ trans('admin/importer/general.select_file') }}
</span>
</label>
<input id="fileupload" type="file" name="files[]" data-url="{{ route('api.imports.index').'?delimiter='.$delimiter }}" accept="text/csv" aria-label="files[]" data-maxsize="{{ Helper::file_upload_max_size() }}" aria-hidden="true">
</span>
</form>
<p class="help-block">{{ trans_choice('general.filetypes_accepted_help', 1, ['size' => Helper::file_upload_max_size_readable(), 'types' => 'csv']) }}</p>
@endif
</div>
</div>
<div class="col-md-3">
<h2>{{ trans('general.importing') }}</h2>
<p>{!! trans('general.importing_help') !!}</p>
</div>
</div>
</div>
@script
<script>
+16 -5
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
@@ -98,14 +98,52 @@
},
formatNoMatches: function () {
return '{{ trans('table.no_matching_records') }}';
}
},
{{--onDrop: function (table, row) {--}}
{{-- var rows = table.tBodies[0].rows;--}}
{{-- var debugStr = "Row dropped was " + row.id + ". New order: ";--}}
{{-- for (var i = 0; i < rows.length; i++) {--}}
{{-- debugStr += rows[i].id + " ";--}}
{{-- }--}}
{{-- --}}
{{-- $.ajax({--}}
{{-- url: '{{ route('api.reorder') }}',--}}
{{-- type: 'POST',--}}
{{-- data: {--}}
{{-- ids: rows.map(function (row) {--}}
{{-- return row.id;--}}
{{-- })--}}
{{--},--}}
});
});
$(function () {
var debugStr = '';
$('.snipe-table').on('reorder-row.bs.table', function (e, data, row){
console.log('All Data:');
console.log(data);
console.log('Dragged Row Data:');
console.dir(row);
console.log('E:');
console.dir(e);
for (var i = 0; i < data.length; i++) {
debugStr += data[i].id + " ";
}
alert('re-order ID: ' + row.id + ' to ' + debugStr);
});
});
});
// function onReorderDrop (rows, row) {
// var rows = table.tBodies[0].rows;
// var debugStr = "Row dropped was " + row.id + ". New order: ";
// for (var i = 0; i < rows.length; i++) {
// debugStr += rows[i].id + " ";
// }
// }
@@ -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>
+8 -13
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>') !!}
+3 -3
View File
@@ -169,6 +169,8 @@
<div class="form-group {{ $errors->has((isset($fieldname) ? $fieldname : 'file')) ? 'has-error' : '' }}" style="margin-bottom: 0px;">
<div class="col-md-8 col-xs-8">
<!-- displayed on screen -->
<label class="btn btn-default col-md-12 col-xs-12" aria-hidden="true">
<x-icon type="paperclip" />
@@ -185,9 +187,7 @@
<div class="col-md-12">
<p class="label label-default col-md-12" style="font-size: 120%!important; margin-top: 10px; margin-bottom: 10px;" id="uploadFile-info"></p>
<p class="help-block" style="margin-top: 10px;" id="uploadFile-status">
{{ trans_choice('general.filetypes_accepted_help', 1, ['size' => Helper::file_upload_max_size_readable(), 'types' => '.zip']) }}
</p>
<p class="help-block" style="margin-top: 10px;" id="uploadFile-status">{{ trans_choice('general.filetypes_accepted_help', 1, ['size' => Helper::file_upload_max_size_readable(), 'types' => '.zip']) }}</p>
{!! $errors->first('file', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
+1 -1
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()}}
+390 -119
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>
+3 -2
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">
@@ -24,6 +24,7 @@
<div class="box-body">
<div class="table-responsive">
<table
data-columns="{{ \App\Presenters\StatusLabelPresenter::dataTableLayout() }}"
data-cookie-id-table="statuslabelsTable"
@@ -36,6 +37,8 @@
data-show-export="true"
data-show-fullscreen="true"
data-show-refresh="true"
data-use-row-attr-func="true"
data-reorderable-rows="true"
data-sort-order="asc"
data-sort-name="name"
id="statuslabelsTable"
+11 -22
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>
+1 -3
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">
+6 -3
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']
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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,
]);
}
}
@@ -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,
]);
}
}
@@ -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);
}
}
@@ -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,
]);
}
}
@@ -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()))
@@ -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');
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
+11 -5
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');
}
}
@@ -2,7 +2,6 @@
namespace Tests\Feature\Settings;
use App\Models\Asset;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
@@ -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();
}
}
@@ -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');
}
}
@@ -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();
}
}
+21
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;
}
);
}
}
@@ -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,
];
}
}
@@ -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),
];
}
}
@@ -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);
});
});
}
}
@@ -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(),
];
}
}
@@ -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}",
];
}
}
+249
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();
}
}
@@ -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,
];
}
}
@@ -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(),
];
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ class LdapTest extends TestCase
"count" => 1,
0 => [
'sn' => 'Surname',
'firstName' => 'FirstName'
'firstname' => 'FirstName'
]
]
);
+3
View File
@@ -103,6 +103,7 @@ mix
[
"./node_modules/bootstrap-table/dist/bootstrap-table.css",
"./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.css",
"./node_modules/bootstrap-table/dist/extensions/reorder-rows/bootstrap-table-reorder-rows.css",
"./resources/assets/css/dragtable.css",
],
"public/css/dist/bootstrap-table.css"
@@ -148,8 +149,10 @@ mix
'./node_modules/bootstrap-table/dist/extensions/export/bootstrap-table-export.js',
'./node_modules/bootstrap-table/dist/extensions/cookie/bootstrap-table-cookie.js',
'./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.js',
'./node_modules/bootstrap-table/dist/extensions/reorder-rows/bootstrap-table-reorder-rows.js',
'./node_modules/bootstrap-table/dist/extensions/addrbar/bootstrap-table-addrbar.js',
'./resources/assets/js/extensions/jquery.base64.js',
"./node_modules/tablednd/dist/jquery.tablednd.js",
'./node_modules/tableexport.jquery.plugin/tableExport.min.js',
'./node_modules/tableexport.jquery.plugin/libs/jsPDF/jspdf.umd.min.js',
'./resources/assets/js/FileSaver.min.js',