Compare commits

...

450 Commits

Author SHA1 Message Date
snipe
9803e27fc1 Fixed #17642 - location data displaying incrrectly in checkout email intro
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 08:40:41 +01:00
snipe
9b4101855f Undo double-float
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 15:24:15 +01:00
snipe
9253d894d3 Removed XSS-Protection header
@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-XSS-Protection#security_considerations

Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 13:30:53 +01:00
snipe
ebd79f22c7 Merge pull request #17636 from grokability/#17627-custom-fields-sorting
Fixed #17627: custom fields not sorting correctly
2025-08-18 12:47:03 +01:00
snipe
c1b139fb9a Fixed #17627: custom fields not sorting correctly
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 12:31:03 +01:00
snipe
a88bcea8ca Merge pull request #17635 from grokability/#17367-fixed-padlock-icon
Fixed #17367: Small adjustment to css-padlock
2025-08-18 11:25:55 +01:00
snipe
21566560a7 Fixed #17367: Small adjustment to css-padlock
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 11:24:05 +01:00
snipe
e3ca43bf40 Remove use of formatCurrencyOutput for input display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 11:00:19 +01:00
snipe
61abb8d5cb Fixed hardware.bulkedit redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 09:45:02 +01:00
snipe
ecad656551 Merge pull request #17626 from grokability/#17606-s3-url-for-models-on-requestable-view
Fixed #17606 - use `getImageUrl()` to determine if local or S3
2025-08-17 14:54:13 +01:00
snipe
615e6d6e4f Fixes #17606 - use getImageUrl() to determine if local or S3
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:51:52 +01:00
snipe
6dceefb96e Merge pull request #17625 from grokability/#17620-delete-method-custom-fields
Fixed #17620 - delete method custom fields causing method not allowed error
2025-08-17 14:11:17 +01:00
snipe
69eff394fd Removed use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:06:56 +01:00
snipe
a9da3aca81 Combine fields and fieldset exception
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:06:49 +01:00
snipe
91f3556375 Added delete test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:33:53 +01:00
snipe
aab7c3a840 Updated custom fields and fieldset pages to use standard delete modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:33:47 +01:00
snipe
9c823119e3 Added new factories for user custom field permissions
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:31:14 +01:00
snipe
f5128833f6 Updated comments
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:52 +01:00
snipe
2bc144354a Use translations and more standard error bag
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:43 +01:00
snipe
e6fec6ec34 Trim model name for display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:28 +01:00
snipe
53389875bf Merge pull request #17611 from grokability/#9965-fallback-to-category-image-for-consumables
Fixed #9965 - fallback to category images (f there are any) when no c…
2025-08-15 15:07:13 +02:00
snipe
3b243b38c8 Fixed #9965 - fallback to category images (f there are any) when no consumable image is present
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 15:03:09 +02:00
snipe
3d9580808b Merge pull request #17524 from Godmartinz/add-category-and-model-to-checkout-emial
Adds #17507 Category and Model No. to accessory checkout markdown
2025-08-15 14:39:58 +02:00
snipe
2141ee71d4 Merge pull request #17544 from marcusmoore/fixes/custom-field-filter
Fixed invalid custom fields being used for filtering
2025-08-15 14:39:09 +02:00
snipe
01dd07083e Merge pull request #17584 from spencerrlongg/bug/17312-custom-field-checkbox-will-not-clear-if-no-checkboxes-should-be-selected
Fixed #17312 - Fix Nulling Checkboxes
2025-08-15 14:35:37 +02:00
snipe
42a28ea06b Merge pull request #17593 from Godmartinz/add-admin-to-acceptance-emails
FIXED #17380 Adds Admin name to acceptance emails
2025-08-15 14:33:02 +02:00
snipe
180cb6ba8e Merge pull request #17610 from grokability/#17600-add-checkout-date-to-accessory-list
Fixed #17600 - adds checkout date to accessories tab in user view
2025-08-15 14:31:38 +02:00
snipe
a78762e40b Fixed #17600 - adds checkout date to accessories tab in user view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 14:29:55 +02:00
snipe
9797bb19e2 Updated dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 14:23:22 +02:00
snipe
08a9554b3c Merge pull request #17607 from Godmartinz/color-corrections-pt9000
Fixes #17488 more info text colors
2025-08-14 20:39:26 +02:00
Godfrey M
d79bd825ee fix popover text color 2025-08-14 10:51:31 -07:00
Godfrey M
fe3d225cfa fix tests 2025-08-14 09:15:19 -07:00
snipe
376e0db66e Merge pull request #17601 from ubc-cpsc/bugfix/CVE-2025-55166
Fixes CVE-2025-55166
2025-08-13 20:49:41 +02:00
Joël Pittet
5fdabc1a62 Fixes CVE-2025-55166 2025-08-13 11:42:14 -07:00
Godfrey M
dfe2a75d72 adds user that checked out item to acceptance emails 2025-08-12 15:34:46 -07:00
Godfrey M
db58b80d27 Merge branch 'develop' into add-category-and-model-to-checkout-emial
# Conflicts:
#	app/Mail/CheckoutLicenseMail.php
2025-08-12 14:20:08 -07:00
Godfrey M
5cb8aae383 add ternaries 2025-08-12 14:16:46 -07:00
spencerrlongg
817530429b added condition to make sure the request has checkbox 2025-08-12 14:52:52 -05:00
Marcus Moore
4a7b7183d2 Add custom_fields. prefix so custom fields can be filtered against 2025-08-11 14:58:41 -07:00
snipe
94bd39cf23 Merge pull request #17570 from grokability/#10038-add-active-flag-filter
Added sidenav to filter on activated vs inactive users
2025-08-11 20:45:22 +01:00
snipe
4038a22093 Added sidenav to filter on activated vs inactive users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 20:41:55 +01:00
snipe
682baec0c9 Merge pull request #17569 from grokability/#10284-add-mobile-number
Fixed #10284: Added mobile phone to users
2025-08-11 18:49:49 +01:00
snipe
ff91be491d Added mobile to tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:43:37 +01:00
snipe
ef35a0f2f1 Fixed #10284: Added mobile phone to users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:38:22 +01:00
snipe
f12a3bb08b Fixed #10306 - cast purchase cost to a float
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:12:37 +01:00
snipe
c8a5065ffa Merge pull request #17567 from grokability/#11754-nicer-menu-alignment
Fixed #11754: nicer menu alignment for dropdowns
2025-08-11 14:57:59 +01:00
snipe
23da5573f3 Fixed #11754 - nicer top menu dropdown alignment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 14:56:43 +01:00
snipe
b08f985776 Merge pull request #17566 from grokability/partial-fix-for-#17565-standard-layout
Show all icons on location table, even if no results
2025-08-11 14:17:59 +01:00
snipe
9b968baaa7 Show all icons, even if no results
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 14:14:15 +01:00
snipe
07edbe6f1c Add @mckaygerhard as a contributor 2025-08-11 13:08:54 +01:00
snipe
1f55a8b6e3 Added icon and tooltip
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 13:06:37 +01:00
snipe
f6b9e11810 Merge pull request #17538 from mckaygerhard/mail-log-improvements
Mail log for #17491 and some improvements on log errors
2025-08-11 13:05:56 +01:00
snipe
c18a3e4266 Fixed #17562 - bootstrap table formater undefined
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 11:18:20 +01:00
snipe
5840ef1c6f Fixed #17560
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 06:26:15 +01:00
snipe
7974baddf5 Merge pull request #17551 from grokability/move-file-uploads-paths-to-base-controller
Move the object type mapping and such to the base controller to de-dupe
2025-08-11 05:44:39 +01:00
snipe
4bf569758f Cleans up a few rmore outes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 05:05:00 +01:00
snipe
f56fd9bb0b Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:04:33 +01:00
snipe
357ee5fc45 Copy over the old dirs just in case
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:02:37 +01:00
snipe
c6dea085b2 Missed a few
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:01:23 +01:00
snipe
8782c3ecec Merge pull request #17554 from grokability/#13997-add-ldap-sync-via-api
Adds #13997 - API endpoint to sync users via LDAP
2025-08-10 20:30:44 +01:00
snipe
b636cf2ef0 Merge pull request #17555 from grokability/#17490-use-numeric-for-purchase-cost
Fixed #17490: use numeric for purchase cost
2025-08-10 20:30:15 +01:00
snipe
6dee2b8601 Fixed 17490 - currency symbol breaks purchase_cost
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 19:04:52 +01:00
snipe
bcf301ac17 Adds #13997 - API endpoint to sync users via LDAP
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:48:01 +01:00
snipe
bf2120fb31 Use newer file path 2025-08-10 18:26:41 +01:00
snipe
de56b74f3e Merge branch 'develop' into move-file-uploads-paths-to-base-controller 2025-08-10 18:25:47 +01:00
snipe
2f146abe91 Let people upload images on the demo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:20:35 +01:00
snipe
543d41b6ff Merge pull request #17553 from grokability/#17547-asset-model-images-not-loading
Fixed #17547: asset model images not loading
2025-08-10 18:15:57 +01:00
snipe
8da0dd7563 Use strtolower
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:11:39 +01:00
snipe
a2217d7dbc Specify the public disk for creating directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:08:15 +01:00
snipe
ea84728a3f Rename models uploads dir
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:58:11 +01:00
snipe
b2d10f7ccf Rename directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:56:59 +01:00
snipe
b6af25ce99 Fixed #17548 - treeview menu class on people menu
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:20:49 +01:00
snipe
7a9d2454d4 Move the object type mapping and such to the base controller to de-dupe
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 16:30:32 +01:00
snipe
a9254cff02 Merge pull request #17550 from grokability/addded-observer-for-maintenances
Added basic logging for maintenances
2025-08-10 16:00:49 +01:00
snipe
d14b34141c Updated comment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 15:53:53 +01:00
snipe
14bc2cc1ba Added basic logging for maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 15:51:48 +01:00
snipe
a91b54b97a Added buttons to maintenances table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:55:34 +01:00
snipe
ead655e1db Fixed translation
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:52:40 +01:00
snipe
c5f28748f7 Merge pull request #17549 from grokability/rename_title_to_name_for_maintenances
Renamed table from `asset_maintenances` to `maintenances` and `title` to `name` for maintenances
2025-08-10 14:28:51 +01:00
snipe
ee4831cb30 Removed followsRedirects so we can check the status
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:23:41 +01:00
snipe
deb1afd28b Few more replcamenents
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:14:21 +01:00
snipe
9e8eead71e Renamed routes and method names
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:29:48 +01:00
snipe
3f96f7cbd7 Updated file paths for uploads
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:45 +01:00
snipe
dde2e88332 Renamed variables in purge
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:32 +01:00
snipe
ff25015595 Renamed more files
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:14 +01:00
snipe
7d0c695808 Renamed language directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:23:52 +01:00
snipe
906385def9 Renamed directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:23:16 +01:00
snipe
a6c6c7eae9 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:11:50 +01:00
snipe
205725c767 Renamed model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:50 +01:00
snipe
c207efbb35 Rename model in breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:42 +01:00
snipe
c0211e59b3 Renames maintenances presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:23 +01:00
snipe
dd2678cbb9 Rename maintenances path
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:09 +01:00
snipe
e2c87b664e Rename factory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:58 +01:00
snipe
29d4b4dd53 Updated API routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:28 +01:00
snipe
3fba307e55 Updated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:18 +01:00
snipe
7171fa36d8 Added migrations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:27:59 +01:00
snipe
c570f656bf Renamed test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:27:48 +01:00
snipe
a5e37519f5 Merge pull request #17539 from grokability/add-file-uploads-to-maintenances
WIP: Add file uploads to maintenances
2025-08-10 11:13:19 +01:00
snipe
0f88d6eec3 Removed error logging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 11:09:29 +01:00
snipe
651c51bb01 Remove unused statements
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 10:41:46 +01:00
mckaygerhard
0fdbdfd5c2 Improve log error handling regarding notification sending for issue #17491
* when an error is generated when denying checkouts, there are not enough logs
to determine the problem from the email provider
* missing handling of log test mail config, there is none of logs cos there
is no log handling on test email, becouse all the results are just sent to
the http response and no log were writen.
2025-08-08 12:18:04 -04:00
snipe
31056ff858 Added new dirs to restore tool
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:56:07 +01:00
snipe
8d2643696b Deleted unused user file controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:59 +01:00
snipe
e7488d19e9 Fixed name for uploaded files controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:48 +01:00
snipe
2bb3b6d64c Fixed prefixes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:24 +01:00
snipe
5744e48ae8 Added getDisplayNameAttribute() to maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:54:36 +01:00
snipe
82d0a21440 Added to actionlog model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:54:09 +01:00
snipe
58133cffac Updated maintenance model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:37:03 +01:00
snipe
bfd8c2f310 Added fles UI on maintenance page
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:51 +01:00
snipe
30d447c023 Updated urls/routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:35 +01:00
snipe
9a0846b8a6 Added directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:16 +01:00
snipe
3667fcddd7 Mark flappy API rate limiting tests as skipped :(
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 11:37:36 +01:00
snipe
906741d662 Bumped debug to warning
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 11:32:04 +01:00
snipe
12be088c4f Merge pull request #17523 from Godmartinz/fix-create-new-rediret
Fixes #17457 Previous Page redirect option
2025-08-08 09:50:40 +01:00
snipe
6737ba80cd Merge pull request #17489 from grokability/fixes/#17485-handle-alert-menu-better-if-no-alerts
Fixed #17485: nicer alert menu if no items are below qty
2025-08-08 09:50:14 +01:00
snipe
862a3d938e Merge pull request #17543 from Godmartinz/salutation-target-fix
Salutation target fix
2025-08-08 09:49:24 +01:00
snipe
09e82377a5 Merge pull request #17520 from marcusmoore/missing-user-redirect-fix
Fixed potential failure in license checkin due to redirect option
2025-08-08 09:48:43 +01:00
snipe
59470864e7 Merge pull request #17542 from akemidx/assetpolicyclassimport
AssetPolicy class import
2025-08-08 09:40:23 +01:00
Marcus Moore
c95aeb3730 Filter out unallowed columns (custom fields) 2025-08-07 17:25:20 -07:00
Godfrey M
5c55c90d68 add null checks to target 2025-08-07 15:27:50 -07:00
Godfrey M
e47972731b fixed target name for checkouts with licenses and assets 2025-08-07 15:12:23 -07:00
Godfrey M
5851cc9e41 fixed target name for checkouts with licenses and assets 2025-08-07 15:09:38 -07:00
akemidx
6f615230e9 class import 2025-08-07 17:00:28 -04:00
snipe
d91598a25e Merge pull request #17540 from marcusmoore/fixes/asset-serial-validation
Fixed 500 when sending non-string for serial property
2025-08-07 20:53:07 +01:00
snipe
9e416778d9 Merge pull request #17541 from marcusmoore/remove-dump-in-test
Removed debugging dump() in test
2025-08-07 20:52:07 +01:00
Marcus Moore
860a117567 Remove dump in test 2025-08-07 12:50:02 -07:00
Marcus Moore
b8fe3b18d4 Add "string" to serial rules for asset 2025-08-07 12:27:48 -07:00
snipe
40269a724b Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:42:59 +01:00
snipe
ec828318d8 Merge pull request #17417 from marcusmoore/snipe-it-17073-asset-requests-are-not-deleted-when-asset-is-deleted
Fixed #17073 - delete old checkout requests
2025-08-07 18:32:13 +01:00
snipe
d31e7ed534 Use new route
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:24:02 +01:00
snipe
5c2dbe438b Added maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:57 +01:00
snipe
10857635ac Removed unused use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:44 +01:00
snipe
df2545ef80 Updated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:17 +01:00
snipe
f6ff729316 Added new generic files upload controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:22:57 +01:00
snipe
38678803eb Removed unused controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:22:45 +01:00
snipe
67c931f196 Merge pull request #17080 from marcusmoore/allow-id-on-location-select
Allowed setting `id` on location-select component
2025-08-07 18:16:58 +01:00
snipe
1c23092d0e Merge pull request #17537 from grokability/add-maintenance-images-and-files
Fixed #10357: Add maintenance image upload
2025-08-07 17:02:34 +01:00
snipe
a90ff21cbf Cleaned up a few more tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 16:58:44 +01:00
snipe
0ce0cee81f Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 16:53:18 +01:00
Герхард PICCORO Lenz McKAY
f4be5ffb5d Fix workaround for #17491 log error on failed response for mail sending
* Part of bunch of fixes, this fix #17491 where admins at test install cannot see the log of errors for UI test mail button, we can just see that this is the correct form cos other parts of the code manage the exception inside the catch using log interface class
2025-08-07 11:42:17 -04:00
snipe
19958748bf Use image upload request
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:39:12 +01:00
snipe
d6ca8468e3 Use snake case for naming paths
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:39:01 +01:00
snipe
7bccb7718b Added partial and enctype="multipart/form-data for upload
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:38:22 +01:00
snipe
f6b63b5e44 Added image to view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:38:04 +01:00
snipe
9a2c5ff195 Updated/added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:57 +01:00
snipe
3597f759da Updated transformers and presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:45 +01:00
snipe
3ed3b21286 Added maintenance file singleton
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:32 +01:00
snipe
b89b636474 Added migration
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:16 +01:00
snipe
2afc595452 Don’t show license key formatter if no value
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:47:47 +01:00
snipe
c7262f2885 Merge pull request #17532 from grokability/add-available-licenses-back-for-now
[FD-50162] Put remaining seats back on license view for now
2025-08-06 16:35:34 +01:00
snipe
8662aa2277 Put remaining seats back on license view for now
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:33:02 +01:00
snipe
8095e0ab72 Normalize consumables user response
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:25:51 +01:00
snipe
be3c8ddd5c Hotfix for FD-50160
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 23:19:27 +01:00
Godfrey M
ec5b9ce903 adds category and model no to accessory checkout markdown 2025-08-05 12:44:07 -07:00
Godfrey M
bd2acefecc rethought, keeping previous page as an option 2025-08-05 12:29:59 -07:00
Godfrey M
18e49e9067 only redirect to previous page if not creating 2025-08-05 12:05:22 -07:00
snipe
a0d65520a3 Use count() instead of ->count() for user count in print view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:34:59 +01:00
snipe
a35731d9d5 Fixed #17513 - updated language string
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:06:08 +01:00
snipe
9d3623cca6 Merge pull request #17521 from grokability/#17518-add-break-after-sigs
Fixed #17518: Adds printer line break after signatures
2025-08-05 19:02:24 +01:00
snipe
2fe08a721f Do not break the page if it’s the last entry
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:00:57 +01:00
Marcus Moore
7abc3a7d7d Only push to session if user exists 2025-08-05 10:57:07 -07:00
snipe
d4a34f1a3c Adds printer line break after signatures
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 18:50:47 +01:00
snipe
ddda4848d3 Added avif to inline
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 18:13:17 +01:00
snipe
8516856d37 Merge pull request #17456 from spencerrlongg/9511-validation-always-fails-on-encrypted-custom-fields
Fixed #9511 - Validation For Encrypted Custom Fields
2025-08-05 17:45:38 +01:00
snipe
132327594b Merge pull request #17515 from grokability/add-submenu-to-users
Added dropdown menu for users
2025-08-04 22:26:59 +01:00
snipe
d2a2c63070 Added dropdown menu for users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 22:25:23 +01:00
snipe
170a5158fa Merge pull request #17514 from grokability/images-on-cloning
Added ability to copy images on cloning
2025-08-04 21:04:56 +01:00
snipe
1d8493d388 Improved messaging for cloning/editing assets that inherit images
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 20:51:24 +01:00
Marcus Moore
ff39e8bd2c Merge branch 'develop' into snipe-it-17073-asset-requests-are-not-deleted-when-asset-is-deleted 2025-08-04 12:43:03 -07:00
snipe
c3442033da Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 18:49:07 +01:00
snipe
f1dd84edba Added option to clone original images
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 18:47:26 +01:00
snipe
06b040a337 Nicer padding
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 18:41:26 +01:00
snipe
fa546ddc5b Merge pull request #17510 from grokability/fixes-#17498-add-serial-to-acceptance
Fixed #17498 - added serial to user acceptance
2025-08-02 14:46:46 +01:00
snipe
f811352c79 Cleaned up HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 14:46:34 +01:00
snipe
7ed8963b9f Fixed #17498 - added serial to user acceptance
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 14:38:57 +01:00
snipe
a9fc8b79fd Merge pull request #17508 from grokability/add-table-buttons
Add table buttons and admin filter
2025-08-01 23:12:04 +01:00
snipe
afd794b4c7 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:20:17 +01:00
snipe
c4a28f0ec4 Use consistent icon for adding people
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:18:02 +01:00
snipe
db343bf795 Tweaked bootstrap admin indicators
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:15:13 +01:00
snipe
0157043dc5 Added table buttons to user view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:58:48 +01:00
snipe
a947f9bd32 Fixed delete modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:30:18 +01:00
snipe
2a4181c7c3 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:19:18 +01:00
snipe
30192f5b14 Removed extra modal code
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:11:54 +01:00
snipe
c41b5e8844 Fixed license delete check
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:11:40 +01:00
snipe
b27928807b Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 20:44:44 +01:00
snipe
16f1b5e23e Added a few more buttons
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 19:31:25 +01:00
snipe
ed651b6869 Use translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:55:15 +01:00
snipe
b9d925c7aa Carry admin/superadmin into the API request
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:49:58 +01:00
snipe
3650a29381 Added superadmin/admin formatter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:49:37 +01:00
snipe
de84ee3693 Cleaned up asset view table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:47:51 +01:00
snipe
42ba31591d New formatter for icon
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:46:20 +01:00
snipe
a78a243e20 Added admin/superadmin filter to API
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:46:10 +01:00
snipe
38924ced4a Provide the role so we can use it in the javascript
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:45:23 +01:00
snipe
5e8cc66f5c Added scope for admins and superadmins
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:45:07 +01:00
snipe
1353837584 More buttons
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:58:15 +01:00
snipe
7cb5a89523 Added access keys
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:58:07 +01:00
snipe
1db09a7953 Allow category_id in license export by category
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:21:12 +01:00
snipe
bc6aa12dd0 Added buttons to table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:20:55 +01:00
snipe
c3bea88979 Added table button JS
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:20:39 +01:00
snipe
6e85e466b0 Merge pull request #17493 from grokability/gallery-view-for-file-uploads
Use the file uploads API for file listing tables, adds gallery view for file uploads
2025-08-01 13:27:27 +01:00
snipe
3327cc70c9 Revert pageSize
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 12:01:57 +01:00
snipe
c9eac66a93 Tweaked button layout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:58 +01:00
snipe
53e9bd6e48 Use updated formatter
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:49 +01:00
snipe
eaa18e1efb Use existing actionlog methods instead of inline
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:40 +01:00
snipe
afa3dacc31 Check if it’s an accepted/declined file
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:22 +01:00
snipe
c803c4a57a Use new formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:20:35 +01:00
snipe
2d3a53e449 Made existing formatters more flexible, removed unused
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:20:24 +01:00
snipe
5e076754ce Merge pull request #17501 from uberbrady/fix_manufacturer_seeder_button
Fixed #17500 [FD-50045] - Make Manufacturer Seeder button work
2025-07-31 04:09:38 +01:00
Brady Wetherington
927e217961 Fix Manufacturer Seeder button 2025-07-30 09:04:04 -06:00
snipe
80b48101aa Added formatter back
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:19:10 +01:00
snipe
08530e6133 Added icon data-dash to formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:11:35 +01:00
snipe
97130ef6c1 Updated IDs to be less generic
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:11:12 +01:00
snipe
da37feae6d Removed comment
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:42 +01:00
snipe
f96172e61f Updated manifest
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:33 +01:00
snipe
e35477b8db Made modal control more flexible
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:26 +01:00
snipe
cea5560a67 Removed duplicated code for modal handling
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:07:35 +01:00
snipe
311bd5e67e Use placeholder for delete button
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:39 +01:00
snipe
1cfddf2a4c Restore old limit code
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:29 +01:00
snipe
abe58117fe Moved code closer to actions
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:01 +01:00
snipe
ee5f89f70d Fixed pagination
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 22:58:00 +01:00
snipe
4f545ed101 Layout tweaks to template
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 22:57:49 +01:00
snipe
136de4208e Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:45 +01:00
snipe
7650a2c2a7 Sort by created_by desc by default
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:23 +01:00
snipe
c3d1987fac Switch to panel
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:06 +01:00
snipe
12ef78bb1c Added PDF embed
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:43:58 +01:00
snipe
16c4241a6e WHY does this work? It’s not in the docs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:42:35 +01:00
snipe
4992c77818 Updated template
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:43 +01:00
snipe
3a0b1de136 Changed table name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:36 +01:00
snipe
1c3ef02c7b FIX THIS!!!
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:15 +01:00
snipe
f268fe9e80 Added gallery card
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 02:03:12 +01:00
snipe
2ed98c17d4 Added print icon
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 02:03:02 +01:00
snipe
571ae4fbfd Use CSS for nowrap
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 01:20:20 +01:00
snipe
6e61e94e02 New manifest
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:31 +01:00
snipe
6a7972c5a1 Added new formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:18 +01:00
snipe
db4fbe315a Added helper to get media type so we know what kind of lightbox to give it
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:11 +01:00
snipe
f3613d7103 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:35:45 +01:00
snipe
cbbed36428 Added multi-file upload for users (bug)
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:35:35 +01:00
snipe
e86e9697b3 Use plural for item type
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:33:25 +01:00
snipe
fd6b2d5715 Simpler blade component calls
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:33:08 +01:00
snipe
fbb36d1665 Fixed file routes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:45 +01:00
snipe
07be1b8192 Added sorting, updated formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:25 +01:00
snipe
33880393ac Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:00 +01:00
snipe
5123fe7838 Use server side endpoint for filetable blade component
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:31:51 +01:00
snipe
cbe26a365d Made route signature more consistent
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:31:14 +01:00
snipe
f1bb72b2a6 Added custom view extension
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:30:51 +01:00
snipe
2c33654395 Fixed #17485 - nicer alert menu if no items are below qty
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:50:26 +01:00
snipe
dd86de017e Dev assets one more time just for good luck
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:38:27 +01:00
snipe
3eabde9630 Dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:36:22 +01:00
snipe
640c51af31 Merge pull request #17487 from uberbrady/improve_javascript_3
Optimize javascript for smaller files and faster builds (Rebase of #15175)
2025-07-28 17:34:59 +01:00
Brady Wetherington
7167b17d25 Rebased and brought up to current from the original 2025-07-28 09:57:20 -06:00
snipe
8a35948678 Import DB facade
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 16:17:11 +01:00
snipe
0fe63d3fb9 Re-added jquery-ui
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 14:03:12 +01:00
snipe
e4302c3e88 Fixed comment
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 09:13:32 +01:00
snipe
a7df6fb465 Added DB_SOCKET to example env
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 09:11:00 +01:00
snipe
133e7598e0 Merge pull request #17478 from grokability/library-upgrades
Library upgrades
2025-07-28 09:00:02 +01:00
snipe
c1a52ffa75 Bumped jspdf-autotable from ^3.8.4 to ^5.0.2
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:52:52 +01:00
snipe
4f46313388 Bumped tableexport to ^1.33.0
https://www.npmjs.com/package/tableexport.jquery.plugin
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:50:47 +01:00
snipe
03b2cc9cd2 Dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:43:22 +01:00
snipe
1a2bf8dc95 Bumped boostrap table from 1.24.1 to 1.24.2
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:43:17 +01:00
snipe
dd63fbeb84 Moved webpack to dev dependencies
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:38:26 +01:00
snipe
59e435c418 Bumped additional libraries
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:36:13 +01:00
snipe
f89f0a19b5 Updated axios
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:33:07 +01:00
snipe
cbc6ef95cb Removed babel-preset
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:26:52 +01:00
snipe
0ceecc9e1d Removed jquery UI
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:24:16 +01:00
snipe
c816902025 Updated postcss
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:19:56 +01:00
snipe
cfb03cdca0 Updated imagemin JS
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:07:34 +01:00
snipe
266f77b08c Update svg-sanitize
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:02:54 +01:00
snipe
257d58c236 Moved privacy policy link in settings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 03:30:10 +01:00
snipe
015f3d936c Merge pull request #17459 from grokability/#17441-add-status-to-id
Fixed #17441 - hardware listings "remembered" page numbers between statuses
2025-07-24 15:54:33 +01:00
snipe
18d2a0ffd7 Fixed #17441 - added status to table IDs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:47:26 +01:00
snipe
24afde0e46 Updated hash and minor version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:35:33 +01:00
snipe
8499faa55a Fixed #17458 - use item_id instead of target_id for user history
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:29:36 +01:00
snipe
c60dd809b8 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 13:06:57 +01:00
snipe
297b8e33f2 Merge pull request #17436 from Godmartinz/fix-acceptance-markdown
Fixed #17394 - Changes the acceptance letter salutation to target
2025-07-23 22:55:05 +01:00
spencerrlongg
d0593c6b8d remove some commented things 2025-07-23 16:19:32 -05:00
spencerrlongg
8a40d7e35c tests added, regex validation working 2025-07-23 16:12:19 -05:00
Godfrey M
b670b2014c accidentally removed a line 2025-07-23 09:56:19 -07:00
Godfrey M
440e969f52 remove unnecessary spacing 2025-07-23 09:47:03 -07:00
snipe
14b79f2f1c Fixed typo in id name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 17:00:09 +01:00
snipe
00cf49a61f Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 16:10:47 +01:00
snipe
4f534e0e84 Bumped version/hash
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 16:02:51 +01:00
snipe
83a19fbbbf Merge pull request #17454 from uberbrady/de_flake_action_log_tests
Enforce order by ID for actionlog tests
2025-07-23 15:06:09 +01:00
snipe
610cb884fc Merge pull request #17452 from uberbrady/de_flake_tls_cert_file_test
This test was flaky, probably due to the PHP statcache.
2025-07-23 15:00:59 +01:00
snipe
ba92cec62b Merge pull request #17453 from grokability/#17316-checkbox-format-on-checkin-checkout
Fixed #17316 - handle checkboxes correctly in checkin/checkout
2025-07-23 14:56:24 +01:00
Brady Wetherington
d92e961a52 enforce order by ID for actionlog tests 2025-07-23 14:55:42 +01:00
snipe
b13e74756a Fixed #17316 - handle checkboxes correctly in checkin/checkout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 14:51:34 +01:00
Brady Wetherington
4ef3072766 This test was flaky, probably due to the PHP statcache. 2025-07-23 14:15:52 +01:00
snipe
e96e2461d3 Merge pull request #17450 from grokability/copy-decrypted-custom-fields-to-clipboard
Fixed #17447 - decrypt before copying to clipboard
2025-07-23 12:41:02 +01:00
snipe
7a2e2be169 Fixed #17447 - decrypt before copying to clipboard
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 12:39:54 +01:00
snipe
8d2a5a7e4a Added location and defaultLoc to searchable relations in audit log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 12:28:23 +01:00
snipe
b7b0e4fab5 Merge pull request #17447 from Godmartinz/make-custom-fields-copyable
Adds #17133 Copy ability to all Custom fields
2025-07-23 12:11:47 +01:00
Godfrey M
a624a79b30 add terenary 2025-07-22 16:36:19 -07:00
Godfrey M
313135da6f Merge branch 'develop' into make-custom-fields-copyable 2025-07-22 16:26:57 -07:00
Godfrey M
58d27d1247 move copy button to front 2025-07-22 16:17:52 -07:00
snipe
edfb28168f Merge pull request #17446 from marcusmoore/snipe-it-17445-move-jobtitle-under-assigned_to-in-assettransformer
Fixed #17445 - move jobtitle under assigned_to in AssetTransformer
2025-07-22 20:27:01 +01:00
Godfrey M
8d0e03bb06 fix copy target 2025-07-22 11:57:46 -07:00
Marcus Moore
855f6f77cf Re-add sorting 2025-07-22 11:49:32 -07:00
Godfrey M
6236cffe14 adds copy links for filled custom fields 2025-07-22 11:49:11 -07:00
Marcus Moore
322a71fbb8 Add jobtitleFormatter 2025-07-22 11:37:34 -07:00
Marcus Moore
4d9f8476f3 Update field key in AssetPresenter 2025-07-22 11:07:58 -07:00
Marcus Moore
d7d93b14b2 Move jobtitle under assigned_to 2025-07-22 11:02:26 -07:00
snipe
d1af3ece6e One more tweak to login checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:25:09 +01:00
snipe
8153b20984 Check for demo mode on UI for able to login
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:18:34 +01:00
snipe
a50f605c29 Merge pull request #17443 from grokability/added-not-allowed-cursor
Adds disabled cursor on uneditable fields in user create/edit
2025-07-22 15:13:14 +01:00
snipe
daf23edd10 Adds disabled cursor on uneditable fields in user create/edit
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:10:27 +01:00
snipe
2eaaeb8259 Merge pull request #17423 from grokability/tighter-permissions-on-non-admins
Tighter permissions on non-admins and demo modes
2025-07-22 14:32:50 +01:00
snipe
a02c62d62c Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 14:12:51 +01:00
snipe
e0232a8e84 Renamed gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 14:02:18 +01:00
snipe
6ea5693b2f Updated comment, removed log error statement
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:59:58 +01:00
snipe
030c2114d1 Merge pull request #17442 from grokability/user-api-eula-fix
Fixed FD-49886 - Optimize user queries
2025-07-22 13:39:36 +01:00
snipe
2cb18e3668 Remove fields from query - eulas was querying actionlogs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:25:41 +01:00
snipe
cd9f8be563 Optimize for when we already have the counts
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:25:16 +01:00
snipe
a02792e9bf Merge pull request #17300 from uberbrady/add_actionlog_tests
Fixed #17071 - Adding various tests of the contents of ActionLogs for lots of events
2025-07-22 10:51:30 +01:00
snipe
41bb422244 Merge pull request #17439 from marcusmoore/component-file-test-fix
Attempt to fix flaky file upload tests pt2
2025-07-21 23:16:03 +01:00
Marcus Moore
54663d3342 Pass order to api in test 2025-07-21 15:10:35 -07:00
snipe
2529f7369f Merge pull request #17438 from grokability/file-upload-tests-fix
Attempt to fix flaky file upload tests
2025-07-21 22:48:38 +01:00
snipe
909c33dccf Fixed order location
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 22:45:17 +01:00
snipe
1adc9f1aa9 Attempt to fix flaky tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 22:18:15 +01:00
spencerrlongg
e9948f0718 fixes booleans, adds note, changes name 2025-07-21 15:34:08 -05:00
Godfrey M
49da9e58fd changed markdown to point to assignedto name 2025-07-21 12:00:00 -07:00
spencerrlongg
2f74a8afe1 mac address rule working 2025-07-21 12:02:45 -05:00
snipe
f3e288d078 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 17:46:49 +01:00
snipe
988000952e Fixed RB-3997
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 13:48:01 +01:00
snipe
6537f3794b Merge pull request #17292 from Godmartinz/fail_with_inputs
FIXED: #17194 Return to bulk edit with errors and inputs
2025-07-21 12:03:52 +01:00
snipe
d31718ba8a Merge pull request #17389 from grokability/use-transformer-for-api-asset-model-response
Use standard model transformer for asset model API response
2025-07-21 11:52:25 +01:00
snipe
9dd4bc5fa8 Merge pull request #17391 from Godmartinz/add-components-notifications
FIXED: #13844 Adds Webhook and Mail Notifications for Components
2025-07-21 11:51:30 +01:00
snipe
df5f1bd522 Merge pull request #17434 from grokability/dependabot/github_actions/develop/codacy/codacy-analysis-cli-action-4.4.7
Bump codacy/codacy-analysis-cli-action from 4.4.5 to 4.4.7
2025-07-21 11:45:04 +01:00
dependabot[bot]
ddffab9169 Bump codacy/codacy-analysis-cli-action from 4.4.5 to 4.4.7
Bumps [codacy/codacy-analysis-cli-action](https://github.com/codacy/codacy-analysis-cli-action) from 4.4.5 to 4.4.7.
- [Release notes](https://github.com/codacy/codacy-analysis-cli-action/releases)
- [Commits](https://github.com/codacy/codacy-analysis-cli-action/compare/v4.4.5...v4.4.7)

---
updated-dependencies:
- dependency-name: codacy/codacy-analysis-cli-action
  dependency-version: 4.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 09:26:25 +00:00
snipe
0c34073582 Namespace fix for presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 17:17:04 +01:00
snipe
14674947cb Fixed test namespace
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 17:15:51 +01:00
snipe
51bccdbd66 Merge pull request #17424 from marcusmoore/chore/livewire-ugprade
Bumped livewire to v3.6.4
2025-07-18 17:12:14 +01:00
snipe
f0fbb3cf36 Uncomment permissions test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:31:31 +01:00
Brady Wetherington
0cc47aacbe Got tests to pass by making them match our current reality, rather than wishes 2025-07-18 16:14:32 +01:00
snipe
fafd592290 Wrap groups and activated into the other canEditAuthFields gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:03:43 +01:00
snipe
40e754b8c3 Additional criteria for the canEditAuthFields gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:03:22 +01:00
snipe
483301db7a Changed some of the gating logic for demo mode. Sigh.
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:59 +01:00
snipe
218606fbd6 Updated view permissions
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:41 +01:00
snipe
c601b8e62c Updated test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:11 +01:00
snipe
2bd68ec991 Uncommented importer gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:17:25 +01:00
snipe
66842648ed Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:17:10 +01:00
snipe
ce54b9a7b5 Removed duplicate alert
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:16:59 +01:00
Brady Wetherington
8a5f6d2a5d Refactor base test into Trait, clean test output for easier comparison 2025-07-18 13:16:35 +01:00
snipe
1d86a5476f Updated language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 12:45:43 +01:00
snipe
ca4d3f6bce Changed gate name, removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 12:45:32 +01:00
Godfrey M
2812f2ce92 remove log 2025-07-17 15:04:42 -07:00
Godfrey M
5c623db798 fix redirect 2025-07-17 14:57:00 -07:00
Marcus Moore
edaf005fe1 Bump livewire to v3.6.4 2025-07-17 14:15:10 -07:00
snipe
4f6e407247 More consistent language degarding the demo
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:13:13 +01:00
snipe
e30881239c A few more clean ups for demo mode
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:08:50 +01:00
snipe
bbde2cc4b2 Use history blade component
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:04:11 +01:00
snipe
16d18c79d7 Fixed email editable field
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:03:20 +01:00
snipe
a0d2cb8a03 Clearer (if longer) gate name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:47:20 +01:00
snipe
1bb5dc7e69 Added one more test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:40:01 +01:00
Brady Wetherington
58759acfe4 Think I hit _all_ of the tests we need to mess with here 2025-07-17 20:15:01 +01:00
snipe
0cd5136052 Added translations
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:52 +01:00
snipe
b3c6fe5369 Use both new gates in user edit
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:46 +01:00
snipe
599718f84e Use new gates in controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:32 +01:00
snipe
d9a5452388 Defined new gates
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:10 +01:00
snipe
0fe49e04bf Attempt to use a gate here?
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:09:27 +01:00
snipe
a98d3fb4dc Check for the format of the permissions (string, object, array)
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:09:17 +01:00
Godfrey M
8c670d1832 clean up 2025-07-17 12:08:49 -07:00
snipe
c232f490bc Show user log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:08:40 +01:00
snipe
c7280953dd Added/updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:08:32 +01:00
Godfrey M
8f4c606c64 remove var dumps 2025-07-17 12:04:33 -07:00
Godfrey M
6740afab42 radio buttons values return correctly 2025-07-17 11:59:09 -07:00
Godfrey M
5df22b3e6a checkboxes properly check 2025-07-17 11:56:52 -07:00
snipe
3d9d18a0d5 Fixed weird CSS quirk
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 19:22:23 +01:00
Marcus Moore
0102599708 Implement tests 2025-07-16 17:20:28 -07:00
Marcus Moore
960edd4adf Improve clarity 2025-07-16 17:11:00 -07:00
Marcus Moore
3547fa723c Delete requests when asset model is deleted 2025-07-16 17:04:14 -07:00
Marcus Moore
7a456185c6 Add explicit state for assets 2025-07-16 16:57:03 -07:00
Marcus Moore
dd79c3f2d6 Scaffold tests 2025-07-16 16:47:28 -07:00
Marcus Moore
35682d11f0 Add command to clean checkout requests 2025-07-16 14:49:45 -07:00
Marcus Moore
d04b3f0907 Enable test 2025-07-16 13:15:06 -07:00
Marcus Moore
c926358e04 Delete requests when user is deleted 2025-07-16 13:11:59 -07:00
Marcus Moore
856ba52f36 Delete requests when asset is deleted 2025-07-16 12:43:56 -07:00
Marcus Moore
a5bea31154 Scaffold tests 2025-07-16 12:38:08 -07:00
Marcus Moore
2afcc1e384 Add basic tests around asset request index 2025-07-16 12:25:37 -07:00
Godfrey M
fc469707a3 clean up 2025-07-16 10:51:33 -07:00
snipe
77fdc370c7 Merge pull request #17415 from uberbrady/clean_unaccepted_assets_report
[FD-47386, FD-49095] New Artisan command to clean checkout acceptances
2025-07-16 17:34:49 +01:00
snipe
301290fb6d Send emails on acceptance even if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 17:02:04 +01:00
snipe
07fffe2f79 Merge pull request #17410 from grokability/remove-password-from-welcome
Remove password from welcome email, prompt for reset instead
2025-07-16 16:54:07 +01:00
snipe
0227a63fa5 Slightly clearer language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:31:45 +01:00
snipe
27764b863c Updated language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:25:36 +01:00
snipe
032fd75f9e Added default invite password token timeout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:51 +01:00
snipe
0bf4f861f3 Nicer debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:25 +01:00
snipe
fd8f90cb52 Added new password broker for longer toekn lifetime
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:11 +01:00
snipe
b6c6b025c8 Added expiry language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:26 +01:00
snipe
3d89e98d1f Small tweaks to welcome email blade
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:15 +01:00
Brady Wetherington
7c5110ed5d Add more action logs tests everywhere I can think of it. 2025-07-16 16:20:06 +01:00
Brady Wetherington
0a474f48ad WIP: Adding various tests of the contents of ActionLogs for lots of events 2025-07-16 16:20:06 +01:00
Brady Wetherington
c409bfd5be New Artisan command to clean checkout acceptances and a migration that runs it 2025-07-16 16:06:23 +01:00
snipe
39d5d5b2e0 Merge branch 'develop' into remove-password-from-welcome 2025-07-16 15:05:13 +01:00
snipe
8a80d9009d Refomatted hidden array
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 12:24:48 +01:00
Godfrey M
f62b5df566 use ternaries instead of optionals 2025-07-15 15:40:21 -07:00
spencerrlongg
826521f053 added rules, still needs a little more... 2025-07-15 15:21:10 -05:00
spencerrlongg
f9b05bc8de more encryption rules extenting laravel's own 2025-07-15 15:03:51 -05:00
spencerrlongg
b8239e8ed9 use laravel validation methods, email works 2025-07-15 14:17:49 -05:00
Godfrey M
214757ab0b fix mailable 2025-07-15 12:04:36 -07:00
Godfrey M
f130186b37 add Component Checkin Mail 2025-07-15 11:56:34 -07:00
Godfrey M
2244eebc3b add Component Checkout Mail 2025-07-15 11:00:39 -07:00
snipe
4176792f2d Translate field
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 16:32:17 +01:00
snipe
1e6cef52c9 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:17:08 +01:00
snipe
a0f4f30a50 Added try/catch
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:13:33 +01:00
snipe
4cbf6ac393 Re-add /setup crential email
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:20:13 +01:00
snipe
af7425d8e6 Remove unused variable
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:19:12 +01:00
snipe
3fea909d3f Removed send credentials option from user controller
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:14:10 +01:00
snipe
7c37d40677 Use plaintext in the database so that the password will never be valid
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:50 +01:00
snipe
3a97c27350 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:29 +01:00
snipe
e0516a52a8 Formatting change
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:55 +01:00
snipe
a85ec6efb3 Set token in welcome email constructor
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:42 +01:00
snipe
3795c74814 Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:26 +01:00
snipe
27954dc6d3 Use password reset token
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:18 +01:00
snipe
68c4187a09 Removed email creds option from user create
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:11:15 +01:00
snipe
b9834231f3 Remove email credentials chexkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:08:36 +01:00
snipe
2be343ea1c More specific no password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:45 +01:00
snipe
109fe1b62c Use no password as temp password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:18 +01:00
snipe
63d691a63c Removed noisy log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:10:48 +01:00
snipe
6f57d6b876 Merge pull request #17407 from grokability/fixes-signature-pad-chrome
Fixed display of acceptance button if signature is not required
2025-07-15 10:58:34 +01:00
snipe
e0bad99ea1 Fixes display of acceptannce button if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 10:55:30 +01:00
snipe
e39eb09cfb Merge pull request #17390 from Godmartinz/unhandled-redirect-error
FIXED redirect option being NULL
2025-07-10 19:41:40 +01:00
Godfrey M
64d397c3f3 add component notification tests 2025-07-10 11:26:10 -07:00
Godfrey M
465ac1d1e1 remove ternary 2025-07-10 08:39:13 -07:00
Godfrey M
18d6becebc populate other_redirect in store method 2025-07-10 08:36:15 -07:00
snipe
943a4093ad Use standard model transformer for asset model API response
Signed-off-by: snipe <snipe@snipe.net>
2025-07-10 15:42:39 +01:00
snipe
b0917a5131 Merge pull request #17385 from grokability/17383-fix-api-route-path
Fixed #17383 - re-add `/hardware/` as an object type in the file upload API
2025-07-10 13:10:43 +01:00
snipe
0972c4e340 Re-added /hardware/ as viable route for API file uploads
Signed-off-by: snipe <snipe@snipe.net>
2025-07-10 13:06:22 +01:00
Godfrey M
3bbd0fdbcd google notifications fires properly 2025-07-09 17:02:51 -07:00
snipe
43a237bf95 Merge pull request #17378 from grokability/phpcs/models
Code formatting fixes
2025-07-09 21:55:30 +01:00
snipe
95f867b267 Code formatting fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 21:48:53 +01:00
snipe
e96daf469a Better phrasing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 21:00:54 +01:00
snipe
f2cdfe9e47 Normalize textarea for notes in acceptance form
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:58:07 +01:00
snipe
929b67e768 Merge pull request #17376 from grokability/small-tweak-to-acceptance-ui
Better indicate via submit button colors and messaging that something is about to be accepted or declined
2025-07-09 20:21:50 +01:00
snipe
0573dc136a Put the sig check back
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:16:30 +01:00
snipe
48588f6a9e Small UI sugar on the acceptance/signature screen
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:08:19 +01:00
Godfrey M
8214b11da5 MS teams fires properly 2025-07-09 11:44:53 -07:00
Godfrey M
36090bf83e checked in notification fires, updated icon translation usage 2025-07-09 11:35:24 -07:00
Godfrey M
bffb2fe82f checkout notification fires 2025-07-09 11:23:27 -07:00
Godfrey M
500cbf5d92 add component checkout notification, update checkout blade, update listener 2025-07-09 11:12:18 -07:00
snipe
88579b9bf3 Merge pull request #17374 from uberbrady/improve_inline_videos
Fixed [FD-49538 ] - use a Video tag for video files for non-Safari usage
2025-07-09 15:47:19 +01:00
snipe
e8bb9bde99 Fixed #8201 - splits first_name and last_name in user export
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 15:46:42 +01:00
Brady Wetherington
0ee3cca4da Use a Video tag for video files for non-Safari usage 2025-07-09 15:15:53 +01:00
snipe
f89ee6b7f2 Merge pull request #17361 from Godmartinz/return-custom-textarea_input
Fixed #7957 - custom field Textarea input not retaining when switching Asset Models with shared fields
2025-07-08 22:02:07 +01:00
snipe
aebfb52c85 Merge pull request #17362 from Godmartinz/license-redirect-bug
Fixed #17310 - 500 on redirect when checking in a license seat
2025-07-08 21:59:22 +01:00
Godfrey M
667bd7af0e fix checkout_to_type being null when checking in 2025-07-08 13:53:10 -07:00
Godfrey M
3fd9e3ab56 include textareas input return 2025-07-08 12:02:56 -07:00
Godfrey M
c8b213c190 remove some changes, move error bag 2025-06-24 13:10:32 -07:00
Godfrey M
942de9dce5 got validation to redirect back to form and display 2025-06-24 12:42:07 -07:00
Marcus Moore
69b9b0bbc0 Allow setting id within location-select 2025-06-02 15:53:25 -07:00
Marcus Moore
3c1088f030 Improve variable name 2025-06-02 15:49:16 -07:00
1634 changed files with 31426 additions and 314662 deletions

View File

@@ -4189,6 +4189,15 @@
"contributions": [
"code"
]
},
{
"login": "mckaygerhard",
"name": "Герхард PICCORO Lenz McKAY ",
"avatar_url": "https://avatars.githubusercontent.com/u/1571724?v=4",
"profile": "https://github-readme-stats.vercel.app/api?username=mckaygerhard",
"contributions": [
"code"
]
}
]
}

View File

@@ -28,6 +28,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_SOCKET=null
DB_PORT='3306'
DB_DATABASE=snipeit
DB_USERNAME=snipeit
@@ -168,6 +169,7 @@ AWS_DEFAULT_REGION=null
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC

View File

@@ -24,6 +24,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_SOCKET=null
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
@@ -174,6 +175,7 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC

View File

@@ -36,7 +36,7 @@ jobs:
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.5
uses: codacy/codacy-analysis-cli-action@v4.4.7
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations

View File

@@ -68,6 +68,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
class CleanIncorrectCheckoutAcceptances extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-checkout-acceptances';
/**
* The console command description.
*
* @var string
*/
protected $description = "Delete checkout acceptances for checkouts to non-users";
/**
* Execute the console command.
*/
public function handle()
{
$deletions = 0;
$skips = 0;
// This walks *every* checkoutacceptance. That's gnarly. But necessary
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if(is_null($item)) {
$this->info("'Checkoutable' Item is null, going to next record");
return; //'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
}
if(get_class($item) == LicenseSeat::class) {
$item = $item->license;
}
foreach($item->assetlog()->where('action_type','checkout')->get() as $assetlog) {
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
//We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
//now, let's compare the _times_ - are they close?
//I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
//were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
//each other (currently 5)
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { //we're allowing for five _ish_ seconds of slop
$deletions++;
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
return;
} else {
//$this->info("The two records are too far apart");
}
} else {
//$this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
}
}
$skips++;
});
$this->error("Final deletion count: $deletions, and skip count: $skips");
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutRequest;
use Illuminate\Console\Command;
class CleanOldCheckoutRequests extends Command
{
private int $deletions = 0;
private int $skips = 0;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-old-checkout-requests';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes checkout requests that reference deleted assets or users.';
/**
* Execute the console command.
*/
public function handle()
{
$requests = CheckoutRequest::with([
'user' => function ($query) {
$query->withTrashed();
},
'requestedItem' => function ($query) {
$query->withTrashed();
},
])->get();
$this->info("Processing {$requests->count()} checkout requests");
$this->withProgressBar($requests, function ($request) {
if ($this->shouldForceDelete($request)) {
$request->forceDelete();
$this->deletions++;
return;
}
if ($this->shouldSoftDelete($request)) {
$request->delete();
$this->deletions++;
return;
}
$this->skips++;
});
$this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
return 0;
}
private function shouldForceDelete(CheckoutRequest $request)
{
// check if the requestable or user relationship is null
return !$request->requestable || !$request->user;
}
private function shouldSoftDelete(CheckoutRequest $request)
{
return $request->requestable->trashed() || $request->user->trashed();
}
}

View File

@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");

View File

@@ -62,19 +62,19 @@ class Purge extends Command
$assetcount = $assets->count();
$this->info($assets->count().' assets purged.');
$asset_assoc = 0;
$asset_maintenances = 0;
$maintenances = 0;
foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
$asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete();
$asset_maintenances += $asset->assetmaintenances()->count();
$asset->assetmaintenances()->forceDelete();
$maintenances += $asset->maintenances()->count();
$asset->maintenances()->forceDelete();
$asset->forceDelete();
}
$this->info($asset_assoc.' corresponding log records purged.');
$this->info($asset_maintenances.' corresponding maintenance records purged.');
$this->info($maintenances.' corresponding maintenance records purged.');
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($locations->count().' locations purged.');

View File

@@ -243,6 +243,8 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/maintenances',
'storage/private_uploads/models',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
@@ -260,9 +262,10 @@ class RestoreFromBackup extends Command
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assetmodels',
'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',

View File

@@ -133,9 +133,18 @@ class Handler extends ExceptionHandler
// This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.index')
->withErrors($error_array, 'bulk_asset_errors')
->withInput();
}
// This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel())))));
$model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh.
@@ -151,9 +160,7 @@ class Handler extends ExceptionHandler
$route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') {
$route = 'licenses.index';
} elseif ($route === 'customfields.index') {
$route = 'fields.index';
} elseif ($route === 'customfieldsets.index') {
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
$route = 'fields.index';
}

View File

@@ -870,7 +870,7 @@ class Helper
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif') || ($filetype == 'image/webp') || ($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif') || ($filetype == 'image/webp')) {
return $filetype;
}
@@ -878,12 +878,33 @@ class Helper
}
/**
* Check if the file is an image, so we can show a preview
* Check if the file is a video, so we can show a preview
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param File $file
* @return string | Boolean
* @author [B. Wetherington] [<bwetherington@grokability.com>]
* @since [v8.1.18]
*/
public static function checkUploadIsVideo($file)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
return $filetype;
}
return false;
}
/**
* Check if the file is audio, so we can show a preview
*
* @param File $file
* @return string | Boolean
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
*/
public static function checkUploadIsAudio($file)
{
@@ -1522,11 +1543,6 @@ class Helper
// return to previous page
if ($redirect_option === 'back') {
if ($backUrl === route('home')) {
return redirect()->to($backUrl)
->with('warning', trans('general.page_error'));
}
return redirect()->to($backUrl);
}

View File

@@ -43,6 +43,8 @@ class IconHelper
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
case 'mobile':
return 'fas fa-mobile-screen-button';
case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right';
case 'download':
@@ -151,6 +153,7 @@ class IconHelper
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
case 'admin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';

View File

@@ -27,6 +27,36 @@ class StorageHelper
}
}
public static function getMediaType($file_with_path) {
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
$fileinfo = pathinfo($file_with_path);
$extension = strtolower($fileinfo['extension']);
switch ($extension) {
case 'avif':
case 'jpg':
case 'png':
case 'gif':
case 'svg':
case 'webp':
return 'image';
case 'pdf':
return 'pdf';
case 'mp3':
case 'wav':
case 'ogg':
return 'audio';
case 'mp4':
case 'webm':
case 'mov':
return 'video';
default:
return $extension; // Default for unknown types
}
}
return null;
}
/**
* This determines the file types that should be allowed inline and checks their fileinfo extension
@@ -52,7 +82,6 @@ class StorageHelper
'pdf',
'png',
'svg',
'svg',
'wav',
'webm',
'webp',

View File

@@ -77,9 +77,25 @@ class AccessoriesController extends Controller
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
$accessory = $request->handleImages($accessory);
if ($request->has('use_cloned_image')) {
$cloned_model_img = Accessory::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'accessories/'.$new_image_name;
Storage::disk('public')->copy('accessories/'.$cloned_model_img->image, $new_image);
$accessory->image = $new_image_name;
}
} else {
$accessory = $request->handleImages($accessory);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the accessory created?
if ($accessory->save()) {
// Redirect to the new accessory page
@@ -114,11 +130,12 @@ class AccessoriesController extends Controller
$this->authorize('create', Accessory::class);
$cloned = clone $accessory;
$accessory_to_clone = $accessory;
$cloned->id = null;
$cloned->deleted_at = '';
$cloned->location_id = null;
return view('accessories/edit')
->with('cloned_model', $accessory_to_clone)
->with('item', $cloned);
}

View File

@@ -1,132 +0,0 @@
<?php
namespace App\Http\Controllers\Accessories;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Accessory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AccessoriesFilesController extends Controller
{
/**
* Validates and stores files associated with a accessory.
*
* @param UploadFileRequest $request
* @param int $accessoryId
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $accessoryId = null) : RedirectResponse
{
if (config('app.lock_passwords')) {
return redirect()->route('accessories.show', ['accessory'=>$accessoryId])->with('error', trans('general.feature_disabled'));
}
$accessory = Accessory::find($accessoryId);
if (isset($accessory->id)) {
$this->authorize('accessories.files', $accessory);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/accessories')) {
Storage::makeDirectory('private_uploads/accessories', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
//Log the upload to the log
$accessory->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
/**
* Deletes the selected accessory file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $accessoryId
* @param int $fileId
*/
public function destroy($accessoryId = null, $fileId = null) : RedirectResponse
{
if ($accessory = Accessory::find($accessoryId)) {
$this->authorize('update', $accessory);
if ($log = Actionlog::find($fileId)) {
if (Storage::exists('private_uploads/accessories/'.$log->filename)) {
try {
Storage::delete('private_uploads/accessories/' . $log->filename);
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
} catch (\Exception $e) {
Log::debug($e);
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist'));
}
}
}
return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $accessoryId
* @param int $fileId
*/
public function show($accessoryId = null, $fileId = null) : View | RedirectResponse | Response | BinaryFileResponse | StreamedResponse
{
// the accessory is valid
if ($accessory = Accessory::find($accessoryId)) {
$this->authorize('view', $accessory);
$this->authorize('accessories.files', $accessory);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
$file = 'private_uploads/accessories/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('accessories.show', ['accessory' => $accessory])->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
}

View File

@@ -232,6 +232,7 @@ class AcceptanceController extends Controller
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
@@ -347,6 +348,7 @@ class AcceptanceController extends Controller
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
@@ -356,13 +358,16 @@ class AcceptanceController extends Controller
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
Log::debug('Attempting to send email acceptance.');
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
$recipient,
$request->input('asset_acceptance') === 'accepted',
));
Log::debug('Send email notification sucess on checkout acceptance response.');
}
} catch (Exception $e) {
Log::error($e->getMessage());
Log::warning($e);
}
}

View File

@@ -154,7 +154,7 @@ class AssetModelsController extends Controller
$assetmodel = $request->handleImages($assetmodel);
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
@@ -207,7 +207,7 @@ class AssetModelsController extends Controller
$assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all());
$assetmodel = $request->handleImages($assetmodel);
/**
* Allow custom_fieldset_id to override and populate fieldset_id.
* This is stupid, but required for legacy API support.
@@ -222,7 +222,7 @@ class AssetModelsController extends Controller
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));

View File

@@ -117,15 +117,20 @@ class AssetsController extends Controller
'jobtitle',
];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
}
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
}
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
}
$assets = Asset::select('assets.*')

View File

@@ -228,11 +228,16 @@ class ConsumablesController extends Controller
foreach ($consumable->consumableAssignments as $consumable_assignment) {
$rows[] = [
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
'user' => ($consumable_assignment->user) ? [
'id' => (int) $consumable_assignment->user->id,
'name'=> e($consumable_assignment->user->present()->fullName()),
] : null,
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response
'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null,
'created_by' => ($consumable_assignment->adminuser) ? [
'id' => (int) $consumable_assignment->adminuser->id,
'name'=> e($consumable_assignment->adminuser->present()->fullName()),
] : null,
];
}

View File

@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\AssetMaintenancesTransformer;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\MaintenancesTransformer;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Maintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -18,13 +18,13 @@ use Illuminate\Http\JsonResponse;
*
* @version v2.0
*/
class AssetMaintenancesController extends Controller
class MaintenancesController extends Controller
{
/**
* Generates the JSON response for asset maintenances listing view.
*
* @see AssetMaintenancesController::getIndex() method that generates view
* @see MaintenancesController::getIndex() method that generates view
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
@@ -33,7 +33,7 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('view', Asset::class);
$maintenances = AssetMaintenance::select('asset_maintenances.*')
$maintenances = Maintenance::select('maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
if ($request->filled('search')) {
@@ -45,11 +45,11 @@ class AssetMaintenancesController extends Controller
}
if ($request->filled('supplier_id')) {
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
$maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('created_by')) {
$maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
$maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
}
if ($request->filled('asset_maintenance_type')) {
@@ -63,7 +63,7 @@ class AssetMaintenancesController extends Controller
$allowed_columns = [
'id',
'title',
'name',
'asset_maintenance_time',
'asset_maintenance_type',
'cost',
@@ -112,7 +112,7 @@ class AssetMaintenancesController extends Controller
$total = $maintenances->count();
$maintenances = $maintenances->skip($offset)->take($limit)->get();
return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
}
@@ -121,22 +121,23 @@ class AssetMaintenancesController extends Controller
/**
* Validates and stores the new asset maintenance
*
* @see AssetMaintenancesController::getCreate() method for the form
* @see MaintenancesController::getCreate() method for the form
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function store(Request $request) : JsonResponse | array
public function store(ImageUploadRequest $request) : JsonResponse | array
{
$this->authorize('update', Asset::class);
// create a new model instance
$maintenance = new AssetMaintenance();
$maintenance = new Maintenance();
$maintenance->fill($request->all());
$maintenance->created_by = auth()->id();
$maintenance = $request->handleImages($maintenance);
// Was the asset maintenance created?
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
}
@@ -157,11 +158,11 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
if ($maintenance = Maintenance::with('asset')->find($id)) {
// Can this user manage this asset?
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
}
// The asset this miantenance is attached to is not valid or has been deleted
@@ -172,13 +173,13 @@ class AssetMaintenancesController extends Controller
$maintenance->fill($request->all());
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id])));
}
@@ -186,20 +187,20 @@ class AssetMaintenancesController extends Controller
* Delete an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $assetMaintenanceId
* @param int $maintenanceId
* @version v1.0
* @since [v4.0]
*/
public function destroy($assetMaintenanceId) : JsonResponse | array
public function destroy($maintenanceId) : JsonResponse | array
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
$maintenance = Maintenance::findOrFail($maintenanceId);
$assetMaintenance->delete();
$maintenance->delete();
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
}
@@ -208,19 +209,19 @@ class AssetMaintenancesController extends Controller
* View an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $assetMaintenanceId
* @param int $maintenanceId
* @version v1.0
* @since [v4.0]
*/
public function show($assetMaintenanceId) : JsonResponse | array
public function show($maintenanceId) : JsonResponse | array
{
$this->authorize('view', Asset::class);
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
$maintenance = Maintenance::findOrFail($maintenanceId);
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
}
return (new AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
return (new MaintenancesTransformer())->transformMaintenance($maintenance);
}
}

View File

@@ -150,8 +150,11 @@ class SettingsController extends Controller
if (!config('app.lock_passwords')) {
try {
Notification::send(Setting::first(), new MailTest());
Log::debug('Attempting to sending to '.config('mail.reply_to.address'));
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (\Exception $e) {
Log::error('Mail sent error using '.config('mail.reply_to.address') .': '. $e->getMessage());
Log::debug($e);
return response()->json(['message' => $e->getMessage()], 500);
}
}
@@ -315,4 +318,4 @@ class SettingsController extends Controller
}
}
}

View File

@@ -194,7 +194,7 @@ class SuppliersController extends Controller
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Supplier::class);
$supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier);
@@ -202,8 +202,8 @@ class SuppliersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
}
if ($supplier->asset_maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count])));
if ($supplier->maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
}
if ($supplier->licenses_count > 0) {

View File

@@ -7,18 +7,9 @@ use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -28,42 +19,6 @@ class UploadedFilesController extends Controller
{
static $map_object_type = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'licenses' => License::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'licenses' => 'private_uploads/licenses/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/**
* List files for an object
*
@@ -90,19 +45,27 @@ class UploadedFilesController extends Controller
'id',
'filename',
'action_type',
'action_date',
'note',
'created_at',
];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset'));
$uploads = Actionlog::select('action_logs.*')
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
// Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations
// And we generally won't need that here
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
// and we generally won't need that here
if ($request->filled('search')) {
$uploads->where(
@@ -113,8 +76,10 @@ class UploadedFilesController extends Controller
);
}
$total = $uploads->count();
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
}

View File

@@ -20,9 +20,11 @@ use App\Models\Consumable;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
@@ -68,6 +70,7 @@ class UsersController extends Controller
'users.notes',
'users.permissions',
'users.phone',
'users.mobile',
'users.state',
'users.two_factor_enrolled',
'users.two_factor_optin',
@@ -81,7 +84,12 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas')
])->with('manager')
->with('groups')
->with('userloc')
->with('company')
->with('department')
->with('createdBy')
->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
@@ -102,10 +110,26 @@ class UsersController extends Controller
$users = $users->where('users.activated', '=', $request->input('activated'));
}
if ($request->input('admins') == 'true') {
$users = $users->OnlyAdminsAndSuperAdmins();
}
if ($request->input('superadmins') == 'true') {
$users = $users->OnlySuperAdmins();
}
if ($request->filled('company_id')) {
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
if ($request->filled('phone')) {
$users = $users->where('users.phone', '=', $request->input('phone'));
}
if ($request->filled('mobile')) {
$users = $users->where('users.mobile', '=', $request->input('mobile'));
}
if ($request->filled('location_id')) {
$users = $users->where('users.location_id', '=', $request->input('location_id'));
}
@@ -278,6 +302,7 @@ class UsersController extends Controller
'manages_users_count',
'manages_locations_count',
'phone',
'mobile',
'address',
'city',
'state',
@@ -475,8 +500,25 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and pull them out if the current user cannot edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
if ($request->filled('username')) {
$user->username = $request->input('username');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
if ($request->filled('activated')) {
$user->activated = $request->input('activated');
}
}
// We need to use has() instead of filled()
@@ -791,4 +833,37 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
}
/**
* Run the LDAP sync command to import users from LDAP via API.
*
* @author A. Gianotto <snipe@snipe.net>
* @since 8.2.2
*
* @return \Illuminate\Http\JsonResponse
*/
public function syncLdapUsers(Request $request)
{
$this->authorize('update', User::class);
// Call Artisan LDAP import command.
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
// Collect and parse JSON summary.
$ldap_results_json = Artisan::output();
$ldap_results = json_decode($ldap_results_json, true);
if (!$ldap_results) {
return response()->json(Helper::formatStandardApiResponse('error', null,trans('general.no_results')), 200);
}
// Direct user to appropriate status page.
if ($ldap_results['error']) {
return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
}
return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);
}
}

View File

@@ -87,7 +87,20 @@ class AssetModelsController extends Controller
$model->fieldset_id = $request->input('fieldset_id');
}
$model = $request->handleImages($model);
if ($request->has('use_cloned_image')) {
$cloned_model_img = AssetModel::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'models/'.$new_image_name;
Storage::disk('public')->copy('models/'.$cloned_model_img->image, $new_image);
$model->image = $new_image_name;
}
} else {
$model = $request->handleImages($model);
}
if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) {
@@ -271,7 +284,7 @@ class AssetModelsController extends Controller
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
->with('model_id', $model->id)
->with('clone_model', $cloned_model);
->with('cloned_model', $cloned_model);
}

View File

@@ -1,115 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\AssetModel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AssetModelsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $modelId = null) : RedirectResponse
{
if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $model);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$model->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function show(AssetModel $model, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
{
$this->authorize('view', $model);
if (! $log = Actionlog::find($fileId)) {
return response('No matching record for that model/file', 500)
->header('Content-Type', 'text/plain');
}
$file = 'private_uploads/assetmodels/'.$log->filename;
if (! Storage::exists($file)) {
return response('File '.$file.' not found on server', 404)
->header('Content-Type', 'text/plain');
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function destroy(AssetModel $model, $fileId = null) : RedirectResponse
{
$rel_path = 'private_uploads/assetmodels';
$this->authorize('update', $model);
$log = Actionlog::find($fileId);
if ($log) {
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
}

View File

@@ -1,108 +0,0 @@
<?php
namespace App\Http\Controllers\Assets;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use \Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AssetFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $assetId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, Asset $asset) : RedirectResponse
{
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
*/
public function show(Asset $asset, $fileId = null) : View | RedirectResponse | Response | StreamedResponse | BinaryFileResponse
{
$this->authorize('view', $asset);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
$file = 'private_uploads/assets/'.$log->filename;
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('hardware.show', $asset)->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('hardware.show', $asset)->with('error', trans('general.log_record_not_found'));
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
*/
public function destroy(Asset $asset, $fileId = null) : RedirectResponse
{
$this->authorize('update', $asset);
$rel_path = 'private_uploads/assets';
if ($log = Actionlog::find($fileId)) {
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->route('hardware.show', $asset)->with('error', trans('general.log_record_not_found'));
}
}

View File

@@ -157,8 +157,16 @@ class AssetsController extends Controller
$asset->location_id = $request->input('rtd_location_id', null);
}
// Create the image (if one was chosen.)
if ($request->has('image')) {
if ($request->has('use_cloned_image')) {
$cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'assets/'.$new_image_name;
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
$asset->image = $new_image_name;
}
} else {
$asset = $request->handleImages($asset);
}
@@ -226,8 +234,15 @@ class AssetsController extends Controller
$failures[] = join(",", $asset->getErrors()->all());
}
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => 'model' ]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($successes) {
@@ -409,6 +424,9 @@ class AssetsController extends Controller
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
$asset->{$field->db_column} = null;
}
if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
@@ -641,8 +659,9 @@ class AssetsController extends Controller
*/
public function getClone(Asset $asset)
{
$this->authorize('create', $asset);
$this->authorize('create', Asset::class);
$cloned = clone $asset;
$cloned_model = $asset;
$cloned->id = null;
$cloned->asset_tag = '';
$cloned->serial = '';
@@ -652,6 +671,7 @@ class AssetsController extends Controller
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
->with('cloned_model', $cloned_model)
->with('item', $cloned);
}

View File

@@ -161,6 +161,7 @@ class BulkAssetsController extends Controller
$models = $assets->unique('model_id');
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
@@ -196,7 +197,6 @@ class BulkAssetsController extends Controller
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@@ -224,11 +224,8 @@ class BulkAssetsController extends Controller
$error_array = array();
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
@@ -543,7 +540,13 @@ class BulkAssetsController extends Controller
} // end asset foreach
if ($has_errors > 0) {
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
session()->put('bulkedit_ids', $request->input('ids'));
session()->put('bulk_asset_errors',$error_array);
return redirect()
->route('hardware.index')
->with('bulk_asset_errors', $error_array)
->withInput();
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
@@ -735,4 +738,33 @@ class BulkAssetsController extends Controller
}
return false;
}
public function bulkEditForm(): View|RedirectResponse
{
$this->authorize('update', Asset::class);
$asset_ids = session()->pull('bulkedit_ids', []);
if (empty($asset_ids)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$assets = Asset::with('model')->withTrashed()->whereIn('id', $asset_ids)->get();
if ($assets->isEmpty()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
}
$models = $assets->unique('model_id');
$modelNames = [];
foreach ($models as $model) {
$modelNames[] = $model->model->name;
}
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
->with('models', $models->pluck(['model']))
->with('modelNames', $modelNames);
}
}

View File

@@ -88,7 +88,12 @@ class ComponentsController extends Controller
$component = $request->handleImages($component);
session()->put(['redirect_option' => $request->get('redirect_option')]);
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($component->save()) {
return Helper::getRedirectOption($request, $component->id, 'Components')

View File

@@ -1,138 +0,0 @@
<?php
namespace App\Http\Controllers\Components;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Component;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
class ComponentsFilesController extends Controller
{
/**
* Validates and stores files associated with a component.
*
* @param UploadFileRequest $request
* @param int $componentId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $componentId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('components.show', ['component'=>$componentId])->with('error', trans('general.feature_disabled'));
}
$component = Component::find($componentId);
if (isset($component->id)) {
$this->authorize('update', $component);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/components')) {
Storage::makeDirectory('private_uploads/components', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
//Log the upload to the log
$component->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('components.show', $component->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->route('components.show', $component->id)->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('components.index')
->with('error', trans('general.file_does_not_exist'));
}
/**
* Deletes the selected component file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $componentId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($componentId = null, $fileId = null)
{
$component = Component::find($componentId);
// the asset is valid
if (isset($component->id)) {
$this->authorize('update', $component);
$log = Actionlog::find($fileId);
// Remove the file if one exists
if (Storage::exists('components/'.$log->filename)) {
try {
Storage::delete('components/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()->withFragment('files')
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the licence management page
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $componentId
* @param int $fileId
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($componentId = null, $fileId = null)
{
Log::debug('Private filesystem is: '.config('filesystems.default'));
// the component is valid
if ($component = Component::find($componentId)) {
$this->authorize('view', $component);
$this->authorize('components.files', $component);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $component->id)->find($fileId)) {
$file = 'private_uploads/components/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Company;
use App\Models\Consumable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -81,13 +81,29 @@ class ConsumablesController extends Controller
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty');
$consumable->created_by = auth()->id();
$consumable->created_by = auth()->id();
$consumable->notes = $request->input('notes');
$consumable = $request->handleImages($consumable);
if ($request->has('use_cloned_image')) {
$cloned_model_img = Consumable::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'consumables/'.$new_image_name;
Storage::disk('public')->copy('consumables/'.$cloned_model_img->image, $new_image);
$consumable->image = $new_image_name;
}
} else {
$consumable = $request->handleImages($consumable);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
@@ -213,9 +229,10 @@ class ConsumablesController extends Controller
$consumable_to_close = $consumable;
$consumable = clone $consumable_to_close;
$consumable->id = null;
$consumable->image = null;
$consumable->created_by = null;
return view('consumables/edit')->with('item', $consumable);
return view('consumables/edit')
->with('cloned_model', $consumable_to_close)
->with('item', $consumable);
}
}

View File

@@ -1,134 +0,0 @@
<?php
namespace App\Http\Controllers\Consumables;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Consumable;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Consumable\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
class ConsumablesFilesController extends Controller
{
/**
* Validates and stores files associated with a consumable.
*
* @param UploadFileRequest $request
* @param int $consumableId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $consumableId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
}
$consumable = Consumable::find($consumableId);
if (isset($consumable->id)) {
$this->authorize('update', $consumable);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/consumables')) {
Storage::makeDirectory('private_uploads/consumables', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
//Log the upload to the log
$consumable->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('consumables.show', $consumable->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->route('consumables.show', $consumable->id)->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('consumables.index')
->with('error', trans('general.file_does_not_exist'));
}
/**
* Deletes the selected consumable file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $consumableId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($consumableId = null, $fileId = null)
{
$consumable = Consumable::find($consumableId);
// the asset is valid
if (isset($consumable->id)) {
$this->authorize('update', $consumable);
$log = Actionlog::find($fileId);
// Remove the file if one exists
if (Storage::exists('consumables/'.$log->filename)) {
try {
Storage::delete('consumables/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()->withFragment('files')
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the licence management page
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $consumableId
* @param int $fileId
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($consumableId = null, $fileId = null)
{
$consumable = Consumable::find($consumableId);
// the consumable is valid
if (isset($consumable->id)) {
$this->authorize('view', $consumable);
$this->authorize('consumables.files', $consumable);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $consumable->id)->find($fileId)) {
$file = 'private_uploads/consumables/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.file_not_found'));
}
}
// The log record doesn't exist somehow
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -22,6 +22,15 @@
namespace App\Http\Controllers;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\Maintenance;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
@@ -32,6 +41,45 @@ abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
static $map_object_type = [
'accessories' => Accessory::class,
'maintenances' => Maintenance::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'hardware' => Asset::class,
'licenses' => License::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'maintenances' => 'private_uploads/maintenances/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/',
'licenses' => 'private_uploads/licenses/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/models/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'maintenances' => 'maintenance',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'hardware' => 'asset',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
public function __construct()
{
view()->share('signedIn', Auth::check());

View File

@@ -144,10 +144,9 @@ class CustomFieldsController extends Controller
*/
public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse
{
$this->authorize('update', CustomField::class);
$field = CustomField::find($field_id);
$this->authorize('update', $field);
// Check that the field exists - this is mostly related to the demo, where we
// rewrite the data every x minutes, so it's possible someone might be disassociating
// a field from a fieldset just as we're wiping the database
@@ -157,11 +156,12 @@ class CustomFieldsController extends Controller
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])
->with('success', trans('admin/custom_fields/message.field.delete.success'));
} else {
return redirect()->back()->withErrors(['message' => "Field is in use and cannot be deleted."]);
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'))
->withInput();
}
}
return redirect()->back()->withErrors(['message' => "Error deleting field from fieldset"]);
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'));
}
@@ -172,20 +172,16 @@ class CustomFieldsController extends Controller
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v1.8]
*/
public function destroy($field_id) : RedirectResponse
public function destroy(CustomField $field) : RedirectResponse
{
if ($field = CustomField::find($field_id)) {
$this->authorize('delete', $field);
$this->authorize('delete', CustomField::class);
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->withErrors(['message' => 'Field is in-use']);
}
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.in_use'));
}
return redirect()->back()->withErrors(['message' => 'Field does not exist']);
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
}
@@ -198,7 +194,7 @@ class CustomFieldsController extends Controller
*/
public function edit(Request $request, CustomField $field) : View | RedirectResponse
{
$this->authorize('update', $field);
$this->authorize('update', CustomField::class);
$fieldsets = CustomFieldset::get();
$customFormat = '';
if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) {
@@ -228,7 +224,7 @@ class CustomFieldsController extends Controller
*/
public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse
{
$this->authorize('update', $field);
$this->authorize('update', CustomField::class);
$show_in_email = $request->get("show_in_email", 0);
$display_in_user_view = $request->get("display_in_user_view", 0);
@@ -265,7 +261,6 @@ class CustomFieldsController extends Controller
if ($field->save()) {
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {

View File

@@ -87,7 +87,9 @@ class LicenseCheckinController extends Controller
if($licenseSeat->assigned_to != null){
$return_to = User::withTrashed()->find($licenseSeat->assigned_to);
session()->put('checkedInFrom', $return_to->id);
if ($return_to) {
session()->put('checkedInFrom', $return_to->id);
}
} else {
$return_to = Asset::find($licenseSeat->asset_id);
}
@@ -98,7 +100,9 @@ class LicenseCheckinController extends Controller
$licenseSeat->notes = $request->input('notes');
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->get('redirect_option') === 'target'){
session()->put(['checkout_to_type' => 'user']);
}
// Was the asset updated?
if ($licenseSeat->save()) {

View File

@@ -1,132 +0,0 @@
<?php
namespace App\Http\Controllers\Licenses;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\License;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class LicenseFilesController extends Controller
{
/**
* Validates and stores files associated with a license.
*
* @param UploadFileRequest $request
* @param int $licenseId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $licenseId = null)
{
$license = License::find($licenseId);
if (isset($license->id)) {
$this->authorize('update', $license);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/licenses')) {
Storage::makeDirectory('private_uploads/licenses', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/licenses/','license-'.$license->id, $file);
//Log the upload to the log
$license->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('licenses.show', $license->id)->with('success', trans('admin/licenses/message.upload.success'));
}
return redirect()->route('licenses.show', $license->id)->with('error', trans('admin/licenses/message.upload.nofiles'));
}
// Prepare the error message
return redirect()->route('licenses.index')
->with('error', trans('admin/licenses/message.does_not_exist'));
}
/**
* Deletes the selected license file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $licenseId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($licenseId = null, $fileId = null)
{
if ($license = License::find($licenseId)) {
$this->authorize('update', $license);
if ($log = Actionlog::find($fileId)) {
// Remove the file if one exists
if (Storage::exists('licenses/'.$log->filename)) {
try {
Storage::delete('licenses/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()
->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->route('licenses.index')->with('error', trans('general.log_does_not_exist'));
}
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $licenseId
* @param int $fileId
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($licenseId = null, $fileId = null, $download = true)
{
$license = License::find($licenseId);
// the license is valid
if (isset($license->id)) {
$this->authorize('view', $license);
$this->authorize('licenses.files', $license);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $license->id)->find($fileId)) {
$file = 'private_uploads/licenses/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.file_not_found'));
}
}
// The log record doesn't exist somehow
return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -102,7 +102,11 @@ class LicensesController extends Controller
$license->created_by = auth()->id();
$license->min_amt = $request->input('min_amt');
session()->put(['redirect_option' => $request->get('redirect_option')]);
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($license->save()) {
return Helper::getRedirectOption($request, $license->id, 'Licenses')
@@ -304,13 +308,16 @@ class LicensesController extends Controller
$response = new StreamedResponse(function () {
// Open output stream
$handle = fopen('php://output', 'w');
$licenses= License::with('company',
$licenses = License::with('company',
'manufacturer',
'category',
'supplier',
'adminuser',
'assignedusers')
->orderBy('created_at', 'DESC');
'assignedusers');
if (request()->filled('category_id')) {
$licenses = $licenses->where('category_id', request()->input('category_id'));
}
$licenses = $licenses->orderBy('created_at', 'DESC');
Company::scopeCompanyables($licenses)
->chunk(500, function ($licenses) use ($handle) {
$headers = [

View File

@@ -96,7 +96,18 @@ class LocationsController extends Controller
$location->company_id = $request->input('company_id');
}
$location = $request->handleImages($location);
if ($request->has('use_cloned_image')) {
$cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'locations/'.$new_image_name;
Storage::disk('public')->copy('locations/'.$cloned_model_img->image, $new_image);
$location->image = $new_image_name;
}
} else {
$location = $request->handleImages($location);
}
if ($location->save()) {
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success'));
@@ -275,9 +286,9 @@ class LocationsController extends Controller
// unset these values
$location->id = null;
$location->image = null;
return view('locations/edit')
->with('cloned_model', $location_to_clone)
->with('item', $location);
}

View File

@@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class LocationsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, Location $location) : RedirectResponse
{
$this->authorize('update', $location);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/locations')) {
Storage::makeDirectory('private_uploads/locations', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/locations/','location-'.$location->id, $file);
$location->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function show(Location $location, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
{
$this->authorize('view', $location);
if (! $log = Actionlog::find($fileId)) {
return redirect()->back()->withFragment('files')->with('error', 'No matching file record');
}
$file = 'private_uploads/locations/'.$log->filename;
if (! Storage::exists($file)) {
return redirect()->back()->withFragment('files')->with('error', 'No matching file on server');
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function destroy(Location $location, $fileId = null) : RedirectResponse
{
$rel_path = 'private_uploads/locations';
$this->authorize('update', $location);
$log = Actionlog::find($fileId);
if ($log) {
// This should be moved to purge
// if (Storage::exists($rel_path.'/'.$log->filename)) {
// Storage::delete($rel_path.'/'.$log->filename);
// }
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
}

View File

@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Maintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
@@ -17,29 +18,23 @@ use \Illuminate\Http\RedirectResponse;
*
* @version v2.0
*/
class AssetMaintenancesController extends Controller
class MaintenancesController extends Controller
{
/**
* Returns a view that invokes the ajax tables which actually contains
* the content for the asset maintenances listing, which is generated in getDatatable.
*
* @todo This should be replaced with middleware and/or policies
* @see AssetMaintenancesController::getDatatable() method that generates the JSON response
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
* the content for the asset maintenances listing.
*/
public function index() : View
{
$this->authorize('view', Asset::class);
return view('asset_maintenances/index');
return view('maintenances.index');
}
/**
* Returns a form view to create a new asset maintenance.
*
* @see AssetMaintenancesController::postCreate() method that stores the data
* @see MaintenancesController::postCreate() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
@@ -55,21 +50,21 @@ class AssetMaintenancesController extends Controller
$asset->asset_id = $asset->id;
}
return view('asset_maintenances/edit')
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
return view('maintenances/edit')
->with('maintenanceType', Maintenance::getImprovementOptions())
->with('asset', $asset)
->with('item', new AssetMaintenance);
->with('item', new Maintenance);
}
/**
* Validates and stores the new asset maintenance
*
* @see AssetMaintenancesController::getCreate() method for the form
* @see MaintenancesController::getCreate() method for the form
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function store(Request $request) : RedirectResponse
public function store(ImageUploadRequest $request) : RedirectResponse
{
$this->authorize('update', Asset::class);
@@ -78,72 +73,73 @@ class AssetMaintenancesController extends Controller
// Loop through the selected assets
foreach ($assets as $asset) {
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$maintenance = new Maintenance();
$maintenance->supplier_id = $request->input('supplier_id');
$maintenance->is_warranty = $request->input('is_warranty');
$maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes');
// Save the asset maintenance data
$assetMaintenance->asset_id = $asset->id;
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id();
$maintenance->asset_id = $asset->id;
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date');
$maintenance->created_by = auth()->id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
if (($maintenance->completion_date !== null)
&& ($maintenance->start_date !== '')
&& ($maintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
$startDate = Carbon::parse($maintenance->start_date);
$completionDate = Carbon::parse($maintenance->completion_date);
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
$maintenance = $request->handleImages($maintenance);
// Was the asset maintenance created?
if (!$assetMaintenance->save()) {
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
if (!$maintenance->save()) {
return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
}
}
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success'));
->with('success', trans('admin/maintenances/message.create.success'));
}
/**
* Returns a form view to edit a selected asset maintenance.
*
* @see AssetMaintenancesController::postEdit() method that stores the data
* @see MaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function edit(AssetMaintenance $maintenance) : View | RedirectResponse
public function edit(Maintenance $maintenance) : View | RedirectResponse
{
$this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset);
return view('asset_maintenances/edit')
return view('maintenances/edit')
->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
->with('asset_ids', request()->input('asset_ids', []))
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
->with('maintenanceType', Maintenance::getImprovementOptions())
->with('item', $maintenance);
}
/**
* Validates and stores an update to an asset maintenance
*
* @see AssetMaintenancesController::postEdit() method that stores the data
* @see MaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param Request $request
* @param int $assetMaintenanceId
* @param int $maintenanceId
* @version v1.0
* @since [v1.8]
*/
public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse
public function update(ImageUploadRequest $request, Maintenance $maintenance) : View | RedirectResponse
{
$this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset);
@@ -153,7 +149,7 @@ class AssetMaintenancesController extends Controller
$maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes');
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->title = $request->input('title');
$maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date');
@@ -176,10 +172,11 @@ class AssetMaintenancesController extends Controller
$completionDate = Carbon::parse($maintenance->completion_date);
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
$maintenance = $request->handleImages($maintenance);
if ($maintenance->save()) {
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.edit.success'));
->with('success', trans('admin/maintenances/message.edit.success'));
}
return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
@@ -189,11 +186,11 @@ class AssetMaintenancesController extends Controller
* Delete an asset maintenance
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @param int $maintenanceId
* @version v1.0
* @since [v1.8]
*/
public function destroy(AssetMaintenance $maintenance) : RedirectResponse
public function destroy(Maintenance $maintenance) : RedirectResponse
{
$this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset);
@@ -201,19 +198,19 @@ class AssetMaintenancesController extends Controller
$maintenance->delete();
// Redirect to the asset_maintenance management page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.delete.success'));
->with('success', trans('admin/maintenances/message.delete.success'));
}
/**
* View an asset maintenance
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @param int $maintenanceId
* @version v1.0
* @since [v1.8]
*/
public function show(AssetMaintenance $maintenance) : View | RedirectResponse
public function show(Maintenance $maintenance) : View | RedirectResponse
{
return view('asset_maintenances/view')->with('assetMaintenance', $maintenance);
return view('maintenances.view')->with('maintenance', $maintenance);
}
}

View File

@@ -51,7 +51,7 @@ class ManufacturersController extends Controller
$manufacturers_count = Manufacturer::withTrashed()->count();
if ($manufacturers_count == 0) {
Artisan::call('db:seed', ['--class' => 'ManufacturerSeeder']);
Artisan::call('db:seed', ['--class' => 'Database\\Seeders\\ManufacturerSeeder', '--force' => true]);
return redirect()->route('manufacturers.index')->with('success', trans('general.seeding.manufacturers.success'));
}

View File

@@ -9,7 +9,7 @@ use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\AssetMaintenance;
use App\Models\Maintenance;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\CustomField;
@@ -17,13 +17,11 @@ use App\Models\Depreciation;
use App\Models\License;
use App\Models\ReportTemplate;
use App\Models\Setting;
use App\Notifications\CheckoutAssetNotification;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use \Illuminate\Contracts\View\View;
use League\Csv\Reader;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -1038,11 +1036,11 @@ class ReportsController extends Controller
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
*/
public function getAssetMaintenancesReport() : View
public function getMaintenancesReport() : View
{
$this->authorize('reports.view');
return view('reports.asset_maintenances');
return view('reports.maintenances');
}
/**
@@ -1051,11 +1049,11 @@ class ReportsController extends Controller
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
*/
public function exportAssetMaintenancesReport() : Response
public function exportMaintenancesReport() : Response
{
$this->authorize('reports.view');
// Grab all the improvements
$assetMaintenances = AssetMaintenance::with('asset', 'supplier')
$Maintenances = Maintenance::with('asset', 'supplier')
->orderBy('created_at', 'DESC')
->get();
@@ -1063,36 +1061,36 @@ class ReportsController extends Controller
$header = [
trans('admin/hardware/table.asset_tag'),
trans('admin/asset_maintenances/table.asset_name'),
trans('admin/maintenances/table.asset_name'),
trans('general.supplier'),
trans('admin/asset_maintenances/form.asset_maintenance_type'),
trans('admin/asset_maintenances/form.title'),
trans('admin/asset_maintenances/form.start_date'),
trans('admin/asset_maintenances/form.completion_date'),
trans('admin/asset_maintenances/form.asset_maintenance_time'),
trans('admin/asset_maintenances/form.cost'),
trans('admin/maintenances/form.asset_maintenance_type'),
trans('admin/maintenances/form.title'),
trans('admin/maintenances/form.start_date'),
trans('admin/maintenances/form.completion_date'),
trans('admin/maintenances/form.asset_maintenance_time'),
trans('admin/maintenances/form.cost'),
];
$header = array_map('trim', $header);
$rows[] = implode(',', $header);
foreach ($assetMaintenances as $assetMaintenance) {
foreach ($Maintenances as $maintenance) {
$row = [];
$row[] = str_replace(',', '', e($assetMaintenance->asset->asset_tag));
$row[] = str_replace(',', '', e($assetMaintenance->asset->name));
$row[] = str_replace(',', '', e($assetMaintenance->supplier->name));
$row[] = e($assetMaintenance->improvement_type);
$row[] = e($assetMaintenance->title);
$row[] = e($assetMaintenance->start_date);
$row[] = e($assetMaintenance->completion_date);
if (is_null($assetMaintenance->asset_maintenance_time)) {
$row[] = str_replace(',', '', e($maintenance->asset->asset_tag));
$row[] = str_replace(',', '', e($maintenance->asset->name));
$row[] = str_replace(',', '', e($maintenance->supplier->name));
$row[] = e($maintenance->improvement_type);
$row[] = e($maintenance->name);
$row[] = e($maintenance->start_date);
$row[] = e($maintenance->completion_date);
if (is_null($maintenance->asset_maintenance_time)) {
$improvementTime = (int) Carbon::now()
->diffInDays(Carbon::parse($assetMaintenance->start_date), true);
->diffInDays(Carbon::parse($maintenance->start_date), true);
} else {
$improvementTime = (int) $assetMaintenance->asset_maintenance_time;
$improvementTime = (int) $maintenance->asset_maintenance_time;
}
$row[] = $improvementTime;
$row[] = trans('general.currency') . Helper::formatCurrencyOutput($assetMaintenance->cost);
$row[] = trans('general.currency') . Helper::formatCurrencyOutput($maintenance->cost);
$rows[] = implode(',', $row);
}

View File

@@ -1084,6 +1084,7 @@ class SettingsController extends Controller
if (! config('app.lock_passwords')) {
if (Storage::exists($path.'/'.$filename)) {
Log::warning('User '.auth()->user()->username.' is attempting to download backup file: '.$filename);
return StorageHelper::downloader($path.'/'.$filename);
} else {
// Redirect to the backup page
@@ -1111,6 +1112,7 @@ class SettingsController extends Controller
if (Storage::exists($path . '/' . $filename)) {
try {
Log::warning('User '.auth()->user()->username.' is attempting to delete backup file: '.$filename);
Storage::delete($path . '/' . $filename);
return redirect()->route('settings.backups.index')->with('success', trans('admin/settings/message.backup.file_deleted'));
} catch (\Exception $e) {
@@ -1190,7 +1192,7 @@ class SettingsController extends Controller
'--force' => true,
]);
Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename);
Log::warning('User '.auth()->user()->username.' is attempting to restore from: '. storage_path($path).'/'.$filename);
$restore_params = [
'--force' => true,
@@ -1339,9 +1341,11 @@ class SettingsController extends Controller
'name' => config('mail.from.name'),
'email' => config('mail.from.address'),
])->notify(new MailTest());
Log::debug('Attempting to send mail to '.config('mail.from.address'));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('mail_sent.mail_sent')));
} catch (\Exception $e) {
Log::error('Mail sent from '.config('mail.from.address') .' with errors '. $e->getMessage());
Log::debug($e);
return response()->json(Helper::formatStandardApiResponse('success', null, $e->getMessage()));
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -122,7 +121,7 @@ class SuppliersController extends Controller
public function destroy($supplierId) : RedirectResponse
{
$this->authorize('delete', Supplier::class);
if (is_null($supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
if (is_null($supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found'));
}
@@ -130,8 +129,8 @@ class SuppliersController extends Controller
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]));
}
if ($supplier->asset_maintenances_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count]));
if ($supplier->maintenances_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count]));
}
if ($supplier->licenses_count > 0) {

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* This controller provide the health route for
* the Snipe-IT Asset Management application.
*
* @version v1.0
*
* @return \Illuminate\Http\JsonResponse
*/
class UploadedFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to store so we can check permisisons
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $object_type, $id) : RedirectResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', $object);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.upload.success', count($files)));
}
// No files were submitted
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.nofiles'));
}
/**
* Check for permissions and display the file.
* This isn't currently used, but is here for future use.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to show from the action_logs table
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// Check that the file being requested exists for the object
if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id))
{
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.invalid_id'));
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename))
{
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.file_not_found'));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function destroy($object_type, $id, $file_id) : RedirectResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
}
}
// The file doesn't seem to really exist, so report an error
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.error', 1));
}
}

View File

@@ -1,129 +0,0 @@
<?php
namespace App\Http\Controllers\Users;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Storage;
class UserFilesController extends Controller
{
/**
* Return JSON response with a list of user details for the getIndex() view.
*
* @param UploadFileRequest $request
* @param int $userId
* @return string JSON
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
*/
public function store(UploadFileRequest $request, User $user)
{
$this->authorize('update', $user);
$files = $request->file('file');
if (is_null($files)) {
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
foreach ($files as $file) {
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
//Log the uploaded file to the log
$logAction = new Actionlog();
$logAction->item_id = $user->id;
$logAction->item_type = User::class;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('notes');
$logAction->target_id = null;
$logAction->created_at = date("Y-m-d H:i:s");
$logAction->filename = $file_name;
$logAction->action_type = 'uploaded';
if (! $logAction->save()) {
return JsonResponse::create(['error' => 'Failed validation: '.print_r($logAction->getErrors(), true)], 500);
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
}
}
/**
* Delete file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
* @param int $userId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($userId = null, $fileId = null)
{
if ($user = User::find($userId)) {
$this->authorize('delete', $user);
$rel_path = 'private_uploads/users';
if ($log = Actionlog::find($fileId)) {
$filename = $log->filename;
$log->delete();
if (Storage::exists($rel_path.'/'.$filename)) {
Storage::delete($rel_path.'/'.$filename);
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.deletefile.success'));
}
}
// The log record doesn't exist somehow
return redirect()->back()->with('success', trans('admin/users/message.deletefile.success'));
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
}
/**
* Display/download the uploaded file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
* @param int $userId
* @param int $fileId
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show(User $user, $fileId = null)
{
if (empty($fileId)) {
return redirect()->route('users.show')->with('error', 'Invalid file request');
}
$this->authorize('view', $user);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $user->id)->find($fileId)) {
$file = 'private_uploads/users/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.file_not_found'));
}
}
// The log record doesn't exist somehow
return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.log_record_not_found'));
}
}

View File

@@ -13,15 +13,8 @@ use App\Models\Company;
use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory;
@@ -105,6 +98,7 @@ class UsersController extends Controller
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle');
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->department_id = $request->input('department_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
@@ -130,10 +124,15 @@ class UsersController extends Controller
}
$user->permissions = json_encode($permissions_array);
// we have to invoke the
// we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
session()->put(['redirect_option' => $request->get('redirect_option')]);
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($user->save()) {
if ($request->filled('groups')) {
@@ -142,18 +141,6 @@ class UsersController extends Controller
$user->groups()->sync([]);
}
if (($request->input('email_user') == 1) && ($request->filled('email'))) {
// Send the credentials through email
$data = [];
$data['email'] = e($request->input('email'));
$data['username'] = e($request->input('username'));
$data['first_name'] = e($request->input('first_name'));
$data['last_name'] = e($request->input('last_name'));
$data['password'] = e($request->input('password'));
$user->notify(new WelcomeNotification($data));
}
return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.create'));
}
@@ -248,22 +235,17 @@ class UsersController extends Controller
}
}
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Update the user fields
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
@@ -273,8 +255,6 @@ class UsersController extends Controller
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
@@ -283,30 +263,49 @@ class UsersController extends Controller
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Set this here so that we can overwrite it later if the user is an admin or superadmin
$user->activated = $request->input('activated', auth()->user()->is($user) ? 1 : $user->activated);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and only set them if the user has permission to edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $user->location_id]);
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
@@ -440,7 +439,7 @@ class UsersController extends Controller
app('request')->request->set('permissions', $permissions);
$user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($user->id);
$user_to_clone = User::with('userloc')->withTrashed()->find($user->id);
// Make sure they can view this particular user
$this->authorize('view', $user_to_clone);
@@ -455,6 +454,8 @@ class UsersController extends Controller
$user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null;
$user->username = null;
$user->avatar = null;
// Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id');
@@ -470,7 +471,7 @@ class UsersController extends Controller
->with('user', $user)
->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups)
->with('clone_user', $user_to_clone)
->with('cloned_model', $user_to_clone)
->with('item', $user);
}
@@ -512,6 +513,8 @@ class UsersController extends Controller
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'),
trans('admin/users/table.username'),
trans('admin/users/table.email'),
@@ -557,6 +560,8 @@ class UsersController extends Controller
($user->company) ? $user->company->name : '',
$user->jobtitle,
$user->employee_num,
$user->first_name,
$user->last_name,
$user->present()->fullName(),
$user->username,
$user->email,

View File

@@ -26,7 +26,6 @@ class SecurityHeaders
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT,
// since we don't provide any way to IFRAME anything in in the first place.

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exception\NotReadableException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class ImageUploadRequest extends Request
{
@@ -70,19 +71,25 @@ class ImageUploadRequest extends Request
public function handleImages($item, $w = 600, $form_fieldname = 'image', $path = null, $db_fieldname = 'image')
{
$type = strtolower(class_basename(get_class($item)));
$type = class_basename(get_class($item));
if (is_null($path)) {
$path = str_plural($type);
$path = strtolower(str_plural($type));
if ($type == 'assetmodel') {
if ($type == 'AssetModel') {
$path = 'models';
}
if ($type == 'user') {
$path = 'avatars';
}
}
if (!Storage::disk('public')->exists($path)) {
Storage::disk('public')->makeDirectory($path);
}
if ($this->offsetGet($form_fieldname) instanceof UploadedFile) {
@@ -93,10 +100,9 @@ class ImageUploadRequest extends Request
if (isset($image)) {
if (!config('app.lock_passwords')) {
$ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
$file_name = $type.'-'.$form_fieldname.($item->id ?? '-'.$item->id).'-'.str_random(10).'.'.$ext;
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
@@ -138,7 +144,7 @@ class ImageUploadRequest extends Request
// Remove Current image if exists
$item = $this->deleteExistingImage($item, $path, $db_fieldname);
$item->{$db_fieldname} = $file_name;
}
// If the user isn't uploading anything new but wants to delete their old image, do so

View File

@@ -80,9 +80,20 @@ class UploadFileRequest extends Request
{
$attributes = [];
if ($this->file) {
if (($this->file) && (is_array($this->file))) {
for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
try {
if ($this->file[$i]) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
} catch (\Exception $e) {
$attributes['file.'.$i] = 'Invalid file';
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CustomField;
@@ -16,6 +17,7 @@ use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ActionlogsTransformer
{
@@ -133,24 +135,6 @@ class ActionlogsTransformer
$clean_meta= $this->changedInfo($clean_meta);
}
$file_url = '';
if($actionlog->filename!='') {
if ($actionlog->action_type == 'accepted') {
$file_url = route('log.storedeula.download', ['filename' => $actionlog->filename]);
} else {
if ($actionlog->item) {
if ($actionlog->itemType() == 'asset') {
$file_url = route('show/assetfile', ['asset' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'accessory') {
$file_url = route('show.accessoryfile', ['accessoryId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'license') {
$file_url = route('show.licensefile', ['licenseId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'user') {
$file_url = route('show/userfile', ['user' => $actionlog->item->id, 'fileId' => $actionlog->id]);
}
}
}
}
$array = [
'id' => (int) $actionlog->id,
@@ -158,8 +142,10 @@ class ActionlogsTransformer
'file' => ($actionlog->filename!='')
?
[
'url' => $file_url,
'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null,
'item' => ($actionlog->item) ? [

View File

@@ -80,7 +80,6 @@ class AssetsTransformer
'qr' => ($setting->qr_code=='1') ? config('app.url').'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png' : null,
'alt_barcode' => ($setting->alt_barcode_enabled=='1') ? config('app.url').'/uploads/barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png' : null,
'assigned_to' => $this->transformAssignedTo($asset),
'jobtitle' => $asset->assigned ? e($asset->assigned->jobtitle) : null,
'warranty_months' => ($asset->warranty_months > 0) ? e($asset->warranty_months.' '.trans('admin/hardware/form.months')) : null,
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [
@@ -204,6 +203,7 @@ class AssetsTransformer
'last_name'=> ($asset->assigned->last_name) ? e($asset->assigned->last_name) : null,
'email'=> ($asset->assigned->email) ? e($asset->assigned->email) : null,
'employee_number' => ($asset->assigned->employee_num) ? e($asset->assigned->employee_num) : null,
'jobtitle' => $asset->assigned->jobtitle ? e($asset->assigned->jobtitle) : null,
'type' => 'user',
] : null;
}

View File

@@ -25,7 +25,7 @@ class ConsumablesTransformer
$array = [
'id' => (int) $consumable->id,
'name' => e($consumable->name),
'image' => ($consumable->image) ? Storage::disk('public')->url('consumables/'.e($consumable->image)) : null,
'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
'category' => ($consumable->category) ? ['id' => $consumable->category->id, 'name' => e($consumable->category->name)] : null,
'company' => ($consumable->company) ? ['id' => (int) $consumable->company->id, 'name' => e($consumable->company->name)] : null,
'item_no' => e($consumable->item_no),

View File

@@ -62,7 +62,7 @@ class LicensesTransformer
'checkin' => Gate::allows('checkin', License::class),
'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class),
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count > 0)) ? true : false,
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
];
$array += $permissions_array;

View File

@@ -4,23 +4,24 @@ namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Maintenance;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Storage;
class AssetMaintenancesTransformer
class MaintenancesTransformer
{
public function transformAssetMaintenances(Collection $assetmaintenances, $total)
public function transformMaintenances(Collection $maintenances, $total)
{
$array = [];
foreach ($assetmaintenances as $assetmaintenance) {
$array[] = self::transformAssetMaintenance($assetmaintenance);
foreach ($maintenances as $assetmaintenance) {
$array[] = self::transformMaintenance($assetmaintenance);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformAssetMaintenance(AssetMaintenance $assetmaintenance)
public function transformMaintenance(Maintenance $assetmaintenance)
{
$array = [
'id' => (int) $assetmaintenance->id,
@@ -33,6 +34,7 @@ class AssetMaintenancesTransformer
'created_at' => Helper::getFormattedDateObject($assetmaintenance->asset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->asset->updated_at, 'datetime'),
] : null,
'image' => ($assetmaintenance->image != '') ? Storage::disk('public')->url('maintenances/'.e($assetmaintenance->image)) : null,
'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
'id' => (int) $assetmaintenance->asset->model->id,
'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
@@ -48,7 +50,8 @@ class AssetMaintenancesTransformer
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
] : null,
'title' => ($assetmaintenance->title) ? e($assetmaintenance->title) : null,
'name' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null,
'title' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null, // legacy to not change the shape of the API
'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [
'id' => (int) $assetmaintenance->asset->location->id,
'name'=> e($assetmaintenance->asset->location->name),

View File

@@ -32,10 +32,11 @@ class UploadedFilesTransformer
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
'type' => str_plural(strtolower(class_basename($file->item_type))),
] : null,
'filename' => e($file->filename),
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'mediatype' => StorageHelper::getMediaType($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [
@@ -44,7 +45,7 @@ class UploadedFilesTransformer
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'inlineable' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
];

View File

@@ -22,6 +22,12 @@ class UsersTransformer
public function transformUser(User $user)
{
$role = '';
if ($user->isSuperUser()) {
$role = 'superadmin';
} elseif ($user->isAdmin()) {
$role = 'admin';
}
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
@@ -39,6 +45,7 @@ class UsersTransformer
'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null,
'vip' => ($user->vip == '1') ? true : false,
'phone' => ($user->phone) ? e($user->phone) : null,
'mobile' => ($user->mobile) ? e($user->mobile) : null,
'website' => ($user->website) ? e($user->website) : null,
'address' => ($user->address) ? e($user->address) : null,
'city' => ($user->city) ? e($user->city) : null,
@@ -59,6 +66,7 @@ class UsersTransformer
'name'=> e($user->userloc->name),
] : null,
'notes'=> Helper::parseEscapedMarkedownInline($user->notes),
'role' => $role,
'permissions' => $user->decodePermissions(),
'activated' => ($user->activated == '1') ? true : false,
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,

View File

@@ -133,7 +133,7 @@ abstract class Importer
} else {
$this->csv = Reader::createFromString($file);
}
$this->tempPassword = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40);
$this->tempPassword = '*** NO PASSWORD - IMPORTED VIA CSV ***';
}
// Cached Values for import lookups

View File

@@ -7,7 +7,9 @@ use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
/**
* This is ONLY used for the User Import. When we are importing users
@@ -50,6 +52,7 @@ class UserImporter extends ItemImporter
$this->item['email'] = trim($this->findCsvMatch($row, 'email'));
$this->item['gravatar'] = trim($this->findCsvMatch($row, 'gravatar'));
$this->item['phone'] = trim($this->findCsvMatch($row, 'phone_number'));
$this->item['mobile'] = trim($this->findCsvMatch($row, 'mobile_number'));
$this->item['website'] = trim($this->findCsvMatch($row, 'website'));
$this->item['jobtitle'] = trim($this->findCsvMatch($row, 'jobtitle'));
$this->item['address'] = trim($this->findCsvMatch($row, 'address'));
@@ -80,6 +83,7 @@ class UserImporter extends ItemImporter
$this->item['username'] = $user_formatted_array['username'];
}
// Check if a numeric ID was passed. If it does, use that above all else.
if ((array_key_exists('id', $this->item) && ($this->item['id'] != "") && (is_numeric($this->item['id'])))) {
$user = User::find($this->item['id']);
@@ -89,12 +93,25 @@ class UserImporter extends ItemImporter
if ($user) {
// If the user does not want to update existing values, only add new ones, bail out
if (! $this->updating) {
Log::debug('A matching User '.$this->item['name'].' already exists. ');
return;
}
$this->log('Updating User');
// Todo - check that this works
if (!Gate::allows('canEditAuthFields', $user)) {
unset($user->username);
unset($user->email);
unset($user->password);
unset($user->activated);
}
$user->update($this->sanitizeItemForUpdating($user));
// Why do we have to do this twice? Update should
$user->save();
// Update the location of any assets checked out to this user
@@ -110,28 +127,32 @@ class UserImporter extends ItemImporter
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
$this->item['password'] = bcrypt($this->tempPassword);
$this->item['password'] = $this->tempPassword;
$this->log('No matching user, creating one');
$user = new User();
$user->created_by = auth()->id();
$user->fill($this->sanitizeItemForStoring($user));
// TODO - check for gate here I guess
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
if (($user->email) && ($user->activated == '1')) {
$data = [
'email' => $user->email,
'username' => $user->username,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'password' => $this->tempPassword,
];
if ($this->send_welcome) {
$user->notify(new WelcomeNotification($data));
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
}
$user = null;
$this->item = null;
@@ -140,9 +161,9 @@ class UserImporter extends ItemImporter
}
$this->logError($user, 'User');
return;
}
/**
* Fetch an existing department, or create new if it doesn't exist
*

View File

@@ -4,10 +4,12 @@ namespace App\Listeners;
use App\Events\CheckoutableCheckedOut;
use App\Mail\CheckinAccessoryMail;
use App\Mail\CheckinComponentMail;
use App\Mail\CheckinLicenseMail;
use App\Mail\CheckoutAccessoryMail;
use App\Mail\CheckoutAssetMail;
use App\Mail\CheckinAssetMail;
use App\Mail\CheckoutComponentMail;
use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory;
@@ -22,9 +24,11 @@ use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckinComponentNotification;
use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutComponentNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException;
@@ -39,7 +43,7 @@ use Osama\LaravelTeamsNotification\TeamsNotification;
class CheckoutableListener
{
private array $skipNotificationsFor = [
Component::class,
// Component::class,
];
/**
@@ -145,7 +149,6 @@ class CheckoutableListener
$shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
$shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
$shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return;
}
@@ -269,6 +272,9 @@ class CheckoutableListener
case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class;
break;
case Component::class:
$notificationClass = CheckinComponentNotification::class;
break;
}
Log::debug('Notification class: '.$notificationClass);
@@ -299,6 +305,9 @@ class CheckoutableListener
case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class;
break;
case Component::class:
$notificationClass = CheckoutComponentNotification::class;
break;
}
@@ -310,6 +319,7 @@ class CheckoutableListener
Asset::class => CheckoutAssetMail::class,
LicenseSeat::class => CheckoutLicenseMail::class,
Consumable::class => CheckoutConsumableMail::class,
Component::class => CheckoutComponentMail::class,
];
$mailable= $lookup[get_class($event->checkoutable)];
@@ -322,8 +332,8 @@ class CheckoutableListener
Accessory::class => CheckinAccessoryMail::class,
Asset::class => CheckinAssetMail::class,
LicenseSeat::class => CheckinLicenseMail::class,
Component::class => CheckinComponentMail::class,
];
$mailable= $lookup[get_class($event->checkoutable)];
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
@@ -469,7 +479,8 @@ class CheckoutableListener
return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof Consumable,
$checkoutable instanceof Component => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
};
}

View File

@@ -334,6 +334,7 @@ class Importer extends Component
'manager_username' => trans('general.importer.manager_username'),
'notes' => trans('general.notes'),
'phone_number' => trans('admin/users/table.phone'),
'mobile_number' => trans('admin/users/table.mobile'),
'remote' => trans('admin/users/general.remote'),
'start_date' => trans('general.start_date'),
'state' => trans('general.state'),
@@ -510,6 +511,13 @@ class Importer extends Component
'telephone',
'tel.',
],
'mobile_number' =>
[
'mobile',
'mobile number',
'cell',
'cellphone',
],
'serial' =>
[

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Mail;
use App\Models\Accessory;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckinComponentMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(Component $component, $checkedOutTo, User $checkedInby, $note)
{
$this->item = $component;
$this->target = $checkedOutTo;
$this->admin = $checkedInby;
$this->note = $note;
$this->settings = Setting::getSettings();
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$from = new Address(config('mail.from.address'), config('mail.from.name'));
return new Envelope(
from: $from,
subject: trans('mail.Confirm_component_checkin'),
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'mail.markdown.checkin-component',
with: [
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -13,7 +13,6 @@ use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\SerializesModels;
class CheckoutAssetMail extends Mailable
@@ -24,16 +23,25 @@ class CheckoutAssetMail extends Mailable
/**
* Create a new message instance.
* @throws \Exception
*/
public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note, bool $firstTimeSending = true)
{
$this->item = $asset;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
$this->target = $checkedOutTo;
// Location is a target option, but there are no emails currently associated with locations.
if($this->target instanceof User){
$this->target = $this->target->present()?->fullName();
}
else if($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
}
$this->last_checkout = '';
$this->expected_checkin = '';
@@ -91,7 +99,7 @@ class CheckoutAssetMail extends Mailable
'admin' => $this->admin,
'status' => $this->item->assetstatus?->name,
'note' => $this->note,
'target' => $this->target,
'target' => $this->target->name ?? $this->admin->present()->fullName(),
'fields' => $fields,
'eula' => $eula,
'req_accept' => $req_accept,
@@ -116,7 +124,7 @@ class CheckoutAssetMail extends Mailable
private function getSubject(): string
{
if ($this->firstTimeSending) {
return trans('mail.Asset_Checkout_Notification');
return trans('mail.Asset_Checkout_Notification').' - '.$this->item->asset_tag;
}
return trans('mail.unaccepted_asset_reminder');

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Mail;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckoutComponentMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(Component $component, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
{
$this->item = $component;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->qty = $component->assets->first()?->pivot?->assigned_qty;
$this->settings = Setting::getSettings();
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$from = new Address(config('mail.from.address'), config('mail.from.name'));
return new Envelope(
from: $from,
subject: trans('mail.Confirm_component_delivery'),
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
return new Content(
markdown: 'mail.markdown.checkout-component',
with: [
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => $accept_url,
'qty' => $this->qty,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Mail;
use App\Models\Asset;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\User;
@@ -25,9 +26,16 @@ class CheckoutLicenseMail extends Mailable
$this->item = $licenseSeat;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
$this->target = $checkedOutTo;
if($this->target instanceof User){
$this->target = $this->target->present()?->fullName();
}
elseif($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
}
}
/**

View File

@@ -15,7 +15,7 @@ use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
* @version v1.0
*/
class Accessory extends SnipeModel
{
@@ -56,8 +56,8 @@ class Accessory extends SnipeModel
];
/**
* Accessory validation rules
*/
* Accessory validation rules
*/
public $rules = [
'name' => 'required|min:3|max:255',
'qty' => 'required|integer|min:1',
@@ -71,12 +71,12 @@ class Accessory extends SnipeModel
/**
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
* is not set in the model it will default to true.
*
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
* is not set in the model it will default to true.
*
* @var bool
*/
*/
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
@@ -108,7 +108,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> supplier relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -121,7 +121,7 @@ class Accessory extends SnipeModel
* Sets the requestable attribute on the accessory
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return void
*/
public function setRequestableAttribute($value)
@@ -136,7 +136,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> company relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -148,7 +148,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> location relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -160,7 +160,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> category relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -172,7 +172,7 @@ class Accessory extends SnipeModel
* Returns the action logs associated with the accessory
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -201,8 +201,8 @@ class Accessory extends SnipeModel
*
* It's super-mega-assy, but it's the best I could do for now.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v5.0.0
* @author A. Gianotto <snipe@snipe.net>
* @since v5.0.0
*
* @see \App\Http\Controllers\Api\AccessoriesController\checkedout()
*/
@@ -219,7 +219,7 @@ class Accessory extends SnipeModel
* presenter or service provider
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return string
*/
public function getImageUrl()
@@ -235,7 +235,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> users relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function checkouts()
@@ -248,7 +248,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> admin user relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.13]
* @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -260,7 +260,7 @@ class Accessory extends SnipeModel
* Checks whether or not the accessory has users
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return int
*/
public function hasUsers()
@@ -274,7 +274,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> manufacturer relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -287,7 +287,7 @@ class Accessory extends SnipeModel
* accessory based on the category it belongs to.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return bool
*/
public function checkin_email()
@@ -300,7 +300,7 @@ class Accessory extends SnipeModel
* accept it via email.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return bool
*/
public function requireAcceptance()
@@ -313,7 +313,7 @@ class Accessory extends SnipeModel
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return string
*/
public function getEula()
@@ -333,7 +333,7 @@ class Accessory extends SnipeModel
* Check how many items within an accessory are checked out
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0]
* @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -350,7 +350,7 @@ class Accessory extends SnipeModel
* bad things happen.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return int
*/
public function numRemaining()
@@ -365,8 +365,8 @@ class Accessory extends SnipeModel
/**
* Run after the checkout acceptance was declined by the user
*
* @param User $acceptedBy
* @param string $signature
* @param User $acceptedBy
* @param string $signature
*/
public function declinedCheckout(User $declinedBy, $signature)
{
@@ -392,8 +392,8 @@ class Accessory extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
@@ -410,7 +410,6 @@ class Accessory extends SnipeModel
/**
* Query builder scope to order on created_by name
*
*/
public function scopeOrderByCreatedByName($query, $order)
{
@@ -418,68 +417,68 @@ class Accessory extends SnipeModel
}
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderCompany($query, $order)
{
return $query->leftJoin('companies', 'accessories.company_id', '=', 'companies.id')
->orderBy('companies.name', $order);
->orderBy('companies.name', $order);
}
/**
* Query builder scope to order on category
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
* Query builder scope to order on category
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderCategory($query, $order)
{
return $query->leftJoin('categories', 'accessories.category_id', '=', 'categories.id')
->orderBy('categories.name', $order);
->orderBy('categories.name', $order);
}
/**
* Query builder scope to order on location
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
* Query builder scope to order on location
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderLocation($query, $order)
{
return $query->leftJoin('locations', 'accessories.location_id', '=', 'locations.id')
->orderBy('locations.name', $order);
->orderBy('locations.name', $order);
}
/**
* Query builder scope to order on manufacturer
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
* Query builder scope to order on manufacturer
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderManufacturer($query, $order)
{
return $query->leftJoin('manufacturers', 'accessories.manufacturer_id', '=', 'manufacturers.id')->orderBy('manufacturers.name', $order);
}
/**
* Query builder scope to order on supplier
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
* Query builder scope to order on supplier
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderSupplier($query, $order)
{
return $query->leftJoin('suppliers', 'accessories.supplier_id', '=', 'suppliers.id')->orderBy('suppliers.name', $order);

View File

@@ -16,7 +16,7 @@ use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
* @version v1.0
*/
class AccessoryCheckout extends Model
{
@@ -36,7 +36,7 @@ class AccessoryCheckout extends Model
* Establishes the accessory checkout -> accessory relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessory()
@@ -52,7 +52,7 @@ class AccessoryCheckout extends Model
* Establishes the accessory checkout -> user relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -64,7 +64,7 @@ class AccessoryCheckout extends Model
* Get the target this asset is checked out to
*
* @author [A. Kroeger]
* @since [v7.0]
* @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
@@ -76,7 +76,7 @@ class AccessoryCheckout extends Model
* Gets the lowercased name of the type of target the asset is assigned to
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return string
*/
public function assignedType()
@@ -91,7 +91,7 @@ class AccessoryCheckout extends Model
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Kroeger]
* @since [v7.0]
* @since [v7.0]
*/
public function checkedOutToUser(): bool
{
@@ -127,50 +127,64 @@ class AccessoryCheckout extends Model
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms
* @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
{
$userQuery = User::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
$userQuery = User::where(
function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
}
}
})->select('id');
)->select('id');
$locationQuery = Location::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
$locationQuery = Location::where(
function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
}
})->select('id');
)->select('id');
$assetQuery = Asset::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
$assetQuery = Asset::where(
function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
}
})->select('id');
)->select('id');
$query->where(function ($query) use ($userQuery) {
$query->where('assigned_type', User::class)
->whereIn('assigned_to', $userQuery);
})->orWhere(function($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
})->orWhere(function($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
})->orWhere(function($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
$query->where(
function ($query) use ($userQuery) {
$query->where('assigned_type', User::class)
->whereIn('assigned_to', $userQuery);
}
});
)->orWhere(
function ($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
}
)->orWhere(
function ($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
}
)->orWhere(
function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
}
}
);
return $query;
}

View File

@@ -7,12 +7,13 @@ use App\Presenters\Presentable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
/**
* Model for the Actionlog (the table that keeps a historical log of
* checkouts, checkins, and updates).
*
* @version v1.0
* @version v1.0
*/
class Actionlog extends SnipeModel
{
@@ -74,6 +75,8 @@ class Actionlog extends SnipeModel
'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'],
'assets.location' => ['name'],
'assets.defaultLoc' => ['name'],
'assets.model.manufacturer' => ['name', 'notes'],
'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'],
'licenses.category' => ['name', 'notes'],
@@ -96,29 +99,31 @@ class Actionlog extends SnipeModel
* Override from Builder to automatically add the company
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public static function boot()
{
parent::boot();
static::creating(function (self $actionlog) {
// If the admin is a superadmin, let's see if the target instead has a company.
if (auth()->user() && auth()->user()->isSuperUser()) {
if ($actionlog->target) {
$actionlog->company_id = $actionlog->target->company_id;
} elseif ($actionlog->item) {
$actionlog->company_id = $actionlog->item->company_id;
static::creating(
function (self $actionlog) {
// If the admin is a superadmin, let's see if the target instead has a company.
if (auth()->user() && auth()->user()->isSuperUser()) {
if ($actionlog->target) {
$actionlog->company_id = $actionlog->target->company_id;
} elseif ($actionlog->item) {
$actionlog->company_id = $actionlog->item->company_id;
}
} elseif (auth()->user() && auth()->user()->company) {
$actionlog->company_id = auth()->user()->company_id;
}
} elseif (auth()->user() && auth()->user()->company) {
$actionlog->company_id = auth()->user()->company_id;
}
if ($actionlog->action_date == '') {
$actionlog->action_date = Carbon::now();
}
if ($actionlog->action_date == '') {
$actionlog->action_date = Carbon::now();
}
});
}
);
}
@@ -126,7 +131,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function item()
@@ -138,7 +143,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> company relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -151,7 +156,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> asset relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -163,7 +168,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> license relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -175,7 +180,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumables()
@@ -187,7 +192,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessories()
@@ -199,7 +204,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> components relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
@@ -211,7 +216,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item type relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function itemType()
@@ -227,7 +232,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target type relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function targetType()
@@ -244,7 +249,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> uploads relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
@@ -258,7 +263,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> userlog relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function userlog()
@@ -270,7 +275,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> admin user relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -283,7 +288,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> user relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -296,7 +301,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function target()
@@ -308,7 +313,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> location relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -321,7 +326,7 @@ class Actionlog extends SnipeModel
* Check if the file exists, and if it does, force a download
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return string | false
*/
public function get_src($type = 'assets', $fieldname = 'filename')
@@ -339,7 +344,7 @@ class Actionlog extends SnipeModel
* Saves the log record with the action type
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return bool
*/
public function logaction($actiontype)
@@ -360,7 +365,7 @@ class Actionlog extends SnipeModel
* Calculate the number of days until the next audit
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return int
*/
public function daysUntilNextAudit($monthInterval = 12, $asset = null)
@@ -389,7 +394,7 @@ class Actionlog extends SnipeModel
* Calculate the date of the next audit
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return \Datetime
*/
public function calcNextAuditDate($monthInterval = 12, $asset = null)
@@ -406,8 +411,8 @@ class Actionlog extends SnipeModel
/**
* Gets action logs in chronological order, excluding uploads
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @since v1.0
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @since v1.0
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getListingOfActionLogsChronologicalOrder()
@@ -423,7 +428,7 @@ class Actionlog extends SnipeModel
* Determines what the type of request is so we can log it to the action_log
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.0
* @since v6.3.0
* @return string
*/
public function determineActionSource(): string
@@ -435,7 +440,8 @@ class Actionlog extends SnipeModel
// This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
&& (starts_with(request()->header('authorization'), 'Bearer '))) {
&& (starts_with(request()->header('authorization'), 'Bearer '))
) {
return 'api';
}
@@ -452,59 +458,58 @@ class Actionlog extends SnipeModel
public function uploads_file_url()
{
switch ($this->item_type) {
case Accessory::class:
return route('show.accessoryfile', [$this->item_id, $this->id]);
case Asset::class:
return route('show/assetfile', [$this->item_id, $this->id]);
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case Component::class:
return route('show.componentfile', [$this->item_id, $this->id]);
case License::class:
return route('show.licensefile', [$this->item_id, $this->id]);
case Location::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case User::class:
return route('show/userfile', [$this->item_id, $this->id]);
default:
return null;
if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
return route('log.storedeula.download', ['filename' => $this->filename]);
}
$object = Str::snake(str_plural(str_replace("App\Models\\", '', $this->item_type)));
if ($object == 'asset_models') {
$object = 'models';
}
return route('ui.files.show', [
'object_type' => $object,
'id' => $this->item_id,
'file_id' => $this->id,
]);
}
public function uploads_file_path()
{
if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
return 'private_uploads/eula-pdfs/'.$this->filename;
}
switch ($this->item_type) {
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
case Asset::class:
return 'private_uploads/assets/'.$this->filename;
case AssetModel::class:
return 'private_uploads/assetmodels/'.$this->filename;
case Consumable::class:
return 'private_uploads/consumables/'.$this->filename;
case Component::class:
return 'private_uploads/components/'.$this->filename;
case License::class:
return 'private_uploads/licenses/'.$this->filename;
case Location::class:
return 'private_uploads/locations/'.$this->filename;
case User::class:
return 'private_uploads/users/'.$this->filename;
default:
return null;
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
case Asset::class:
return 'private_uploads/assets/'.$this->filename;
case AssetModel::class:
return 'private_uploads/models/'.$this->filename;
case Consumable::class:
return 'private_uploads/consumables/'.$this->filename;
case Component::class:
return 'private_uploads/components/'.$this->filename;
case License::class:
return 'private_uploads/licenses/'.$this->filename;
case Location::class:
return 'private_uploads/locations/'.$this->filename;
case Maintenance::class:
return 'private_uploads/maintenances/'.$this->filename;
case User::class:
return 'private_uploads/users/'.$this->filename;
default:
return null;
}
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
* Model for Asset Models. Asset Models contain higher level
* attributes that are common among the same type of asset.
*
* @version v1.0
* @version v1.0
*/
class AssetModel extends SnipeModel
{
@@ -98,14 +98,22 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'],
];
protected static function booted(): void
{
static::forceDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->forceDelete();
});
static::softDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->delete();
});
}
/**
* Establishes the model -> assets relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -117,7 +125,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> category relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -129,7 +137,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> depreciation relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function depreciation()
@@ -141,7 +149,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> manufacturer relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -153,7 +161,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> fieldset relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fieldset()
@@ -163,14 +171,14 @@ class AssetModel extends SnipeModel
public function customFields()
{
return $this->fieldset()->first()->fields();
return $this->fieldset()->first()->fields();
}
/**
* Establishes the model -> custom field default values relationship
*
* @author hannah tinkler
* @since [v4.3]
* @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
@@ -184,7 +192,7 @@ class AssetModel extends SnipeModel
* @todo this should probably be moved
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function getImageUrl()
@@ -201,7 +209,7 @@ class AssetModel extends SnipeModel
* Checks if the model is deletable
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.4]
* @since [v6.3.4]
* @return bool
*/
public function isDeletable()
@@ -216,7 +224,7 @@ class AssetModel extends SnipeModel
* Get user who created the item
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -235,10 +243,10 @@ class AssetModel extends SnipeModel
* scopeInCategory
* Get all models that are in the array of category ids
*
* @param $query
* @param $query
* @param array $categoryIdListing
*
* @return mixed
* @return mixed
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
*/
@@ -251,9 +259,9 @@ class AssetModel extends SnipeModel
* scopeRequestable
* Get all models that are requestable by a user.
*
* @param $query
* @param $query
*
* @return $query
* @return $query
* @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @version v3.5
*/
@@ -265,8 +273,8 @@ class AssetModel extends SnipeModel
/**
* Query builder scope to search on text, including catgeory and manufacturer name
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
@@ -274,23 +282,31 @@ class AssetModel extends SnipeModel
{
return $query->where('models.name', 'LIKE', "%$search%")
->orWhere('model_number', 'LIKE', "%$search%")
->orWhere(function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
});
->orWhere(
function ($query) use ($search) {
$query->whereHas(
'category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
}
);
}
)
->orWhere(
function ($query) use ($search) {
$query->whereHas(
'manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
}
);
}
);
}
/**
* Query builder scope to order on manufacturer
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -302,8 +318,8 @@ class AssetModel extends SnipeModel
/**
* Query builder scope to order on category name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -319,7 +335,6 @@ class AssetModel extends SnipeModel
/**
* Query builder scope to order on created_by name
*
*/
public function scopeOrderByCreatedByName($query, $order)
{

View File

@@ -18,7 +18,7 @@ use Illuminate\Support\Str;
* to require acceptance from the user, whether or not to
* send a EULA to the user, etc.
*
* @version v1.0
* @version v1.0
*/
class Category extends SnipeModel
{
@@ -96,7 +96,7 @@ class Category extends SnipeModel
* Checks if category can be deleted
*
* @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>]
* @since [v5.0]
* @since [v5.0]
* @return bool
*/
public function isDeletable()
@@ -119,7 +119,7 @@ class Category extends SnipeModel
* Establishes the category -> accessories relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessories()
@@ -131,7 +131,7 @@ class Category extends SnipeModel
* Establishes the category -> licenses relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.3]
* @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -143,7 +143,7 @@ class Category extends SnipeModel
* Establishes the category -> consumables relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumables()
@@ -155,7 +155,7 @@ class Category extends SnipeModel
* Establishes the category -> consumables relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
@@ -170,7 +170,7 @@ class Category extends SnipeModel
* It should only be used in a single category context.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return int
*/
public function itemCount()
@@ -181,18 +181,18 @@ class Category extends SnipeModel
}
switch ($this->category_type) {
case 'asset':
return $this->assets->count();
case 'accessory':
return $this->accessories->count();
case 'component':
return $this->components->count();
case 'consumable':
return $this->consumables->count();
case 'license':
return $this->licenses->count();
default:
return 0;
case 'asset':
return $this->assets->count();
case 'accessory':
return $this->accessories->count();
case 'component':
return $this->components->count();
case 'consumable':
return $this->consumables->count();
case 'license':
return $this->licenses->count();
default:
return 0;
}
}
@@ -201,7 +201,7 @@ class Category extends SnipeModel
* Establishes the category -> assets relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -218,8 +218,8 @@ class Category extends SnipeModel
* by their category.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.0]
* @see \App\Models\Asset::scopeAssetsForShow()
* @since [v6.1.0]
* @see \App\Models\Asset::scopeAssetsForShow()
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function showableAssets()
@@ -231,7 +231,7 @@ class Category extends SnipeModel
* Establishes the category -> models relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -249,7 +249,7 @@ class Category extends SnipeModel
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @since [v2.0]
* @return string | null
*/
public function getEula()
@@ -276,7 +276,7 @@ class Category extends SnipeModel
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
* @param $value
* @param $value
* @return void
*/
public function setCheckinEmailAttribute($value)
@@ -293,9 +293,9 @@ class Category extends SnipeModel
/**
* Query builder scope for whether or not the category requires acceptance
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @author Vincent Sposato <vincent.sposato@gmail.com>
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeRequiresAcceptance($query)

View File

@@ -66,7 +66,7 @@ class CheckoutAcceptance extends Model
/**
* Was the checkoutable checked out to this user?
*
* @param User $user
* @param User $user
* @return bool
*/
public function isCheckedOutTo(User $user)
@@ -79,7 +79,7 @@ class CheckoutAcceptance extends Model
* Do not add stuff here that doesn't have a corresponding column in the
* checkout_acceptances table or you'll get an error.
*
* @param string $signature_filename
* @param string $signature_filename
*/
public function accept($signature_filename, $eula = null, $filename = null, $note = null)
{
@@ -99,7 +99,7 @@ class CheckoutAcceptance extends Model
/**
* Decline the checkout acceptance
*
* @param string $signature_filename
* @param string $signature_filename
*/
public function decline($signature_filename, $note = null)
{
@@ -116,8 +116,9 @@ class CheckoutAcceptance extends Model
/**
* Filter checkout acceptences by the user
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param User $user
* @param User $user
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser(Builder $query, User $user)
@@ -127,6 +128,7 @@ class CheckoutAcceptance extends Model
/**
* Filter to only get pending acceptances
*
* @param Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/

View File

@@ -2,11 +2,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CheckoutRequest extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = ['user_id'];
protected $table = 'checkout_requests';

View File

@@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Schema;
/**
* Model for Companies.
*
* @version v1.8
* @version v1.8
*/
final class Company extends SnipeModel
{
@@ -28,19 +28,19 @@ final class Company extends SnipeModel
'name' => 'required|min:1|max:255|unique:companies,name',
'fax' => 'min:7|max:35|nullable',
'phone' => 'min:7|max:35|nullable',
'email' => 'email|max:150|nullable',
'email' => 'email|max:150|nullable',
];
protected $presenter = \App\Presenters\CompanyPresenter::class;
use Presentable;
/**
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
* is not set in the model it will default to true.
*
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
* is not set in the model it will default to true.
*
* @var bool
*/
*/
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
use Searchable;
@@ -102,7 +102,7 @@ final class Company extends SnipeModel
* account the full multiple company support setting
* and if the current user is a super user.
*
* @param $unescaped_input
* @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForCurrentUser($unescaped_input)
@@ -129,7 +129,7 @@ final class Company extends SnipeModel
* Check to see if the current user should have access to the model.
* I hate this method and I think it should be refactored.
*
* @param $companyable
* @param $companyable
* @return bool|void
*/
public static function isCurrentUserHasAccess($companyable)
@@ -192,7 +192,7 @@ final class Company extends SnipeModel
* Checks if company can be deleted
*
* @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>]
* @since [v5.0]
* @since [v5.0]
* @return bool
*/
public function isDeletable()
@@ -209,7 +209,7 @@ final class Company extends SnipeModel
}
/**
* @param $unescaped_input
* @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForUser($unescaped_input)
@@ -265,9 +265,9 @@ final class Company extends SnipeModel
* @todo - refactor that trait to handle the user's model as well.
*
* @author [A. Gianotto] <snipe@snipe.net>
* @param $query
* @param $column
* @param $table_name
* @param $query
* @param $column
* @param $table_name
* @return mixed
*/
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
@@ -327,8 +327,8 @@ final class Company extends SnipeModel
* This gets invoked by CompanyableChildScope, but I'm not sure what it does.
*
* @author [A. Gianotto] <snipe@snipe.net>
* @param array $companyable_names
* @param $query
* @param array $companyable_names
* @param $query
* @return mixed
*/
public static function scopeCompanyableChildren(array $companyable_names, $query)
@@ -343,13 +343,15 @@ final class Company extends SnipeModel
static::scopeCompanyablesDirectly($q);
};
$q = $query->where(function ($q) use ($companyable_names, $f) {
$q2 = $q->whereHas($companyable_names[0], $f);
$q = $query->where(
function ($q) use ($companyable_names, $f) {
$q2 = $q->whereHas($companyable_names[0], $f);
for ($i = 1; $i < count($companyable_names); $i++) {
$q2 = $q2->orWhereHas($companyable_names[$i], $f);
for ($i = 1; $i < count($companyable_names); $i++) {
$q2 = $q2->orWhereHas($companyable_names[$i], $f);
}
}
});
);
return $q;
}

View File

@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/**
* Handle query scoping for full company support.
*
* @todo Move this to a more Laravel 5.2 esque way
* @version v1.0
* @todo Move this to a more Laravel 5.2 esque way
* @version v1.0
*/
final class CompanyableChildScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function apply(Builder $builder, Model $model)
@@ -31,7 +31,7 @@ final class CompanyableChildScope implements Scope
* @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function remove(Builder $builder)

View File

@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/**
* Handle query scoping for full company support.
*
* @todo Move this to a more Laravel 5.2 esque way
* @version v1.0
* @todo Move this to a more Laravel 5.2 esque way
* @version v1.0
*/
final class CompanyableScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function apply(Builder $builder, Model $model)
@@ -29,7 +29,7 @@ final class CompanyableScope implements Scope
* @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function remove(Builder $builder)

View File

@@ -8,7 +8,7 @@ trait CompanyableTrait
* This trait is used to scope models to the current company. To use this scope on companyable models,
* we use the "use Companyable;" statement at the top of the mode.
*
* @see \App\Models\Company\Company::scopeCompanyables()
* @see \App\Models\Company\Company::scopeCompanyables()
* @return void
*/
public static function bootCompanyableTrait()

View File

@@ -2,18 +2,20 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
* Model for Components.
*
* @version v1.0
* @version v1.0
*/
class Component extends SnipeModel
{
@@ -121,7 +123,7 @@ class Component extends SnipeModel
* Establishes the component -> location relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -133,7 +135,7 @@ class Component extends SnipeModel
* Establishes the component -> assets relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -147,7 +149,7 @@ class Component extends SnipeModel
* @todo this is probably not needed - refactor
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -159,7 +161,7 @@ class Component extends SnipeModel
* Establishes the component -> company relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -171,7 +173,7 @@ class Component extends SnipeModel
* Establishes the component -> category relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -183,7 +185,7 @@ class Component extends SnipeModel
* Establishes the item -> supplier relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1]
* @since [v6.1.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -196,19 +198,49 @@ class Component extends SnipeModel
* Establishes the item -> manufacturer relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
{
return $this->belongsTo(\App\Models\Manufacturer::class, 'manufacturer_id');
}
/**
* Determine whether this asset requires acceptance by the assigned user
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return bool
*/
public function requireAcceptance()
{
return $this->category->require_acceptance;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string | false
*/
public function getEula()
{
if ($this->category->eula_text) {
return Helper::parseEscapedMarkedown($this->category->eula_text);
} elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
return null;
}
}
/**
* Establishes the component -> action logs relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -220,7 +252,7 @@ class Component extends SnipeModel
* Check how many items within a component are checked out
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0]
* @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -239,20 +271,34 @@ class Component extends SnipeModel
*
* This allows us to get the assets with assigned components without the company restriction
*/
public function uncontrainedAssets() {
public function uncontrainedAssets()
{
return $this->belongsToMany(\App\Models\Asset::class, 'components_assets')
->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
->withoutGlobalScope(new CompanyableScope);
->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
->withoutGlobalScope(new CompanyableScope);
}
/**
* Determine whether to send a checkin/checkout email based on
* asset model category
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return bool
*/
public function checkin_email()
{
return $this->category?->checkin_email;
}
/**
* Check how many items within a component are remaining
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return int
*/
public function numRemaining()
@@ -275,8 +321,8 @@ class Component extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
@@ -294,8 +340,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -307,8 +353,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -320,8 +366,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -333,8 +379,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on supplier
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -346,8 +392,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on manufacturer
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/

View File

@@ -125,7 +125,7 @@ class Consumable extends SnipeModel
* @todo Update this comment once it's been implemented
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function setRequestableAttribute($value)
@@ -140,7 +140,7 @@ class Consumable extends SnipeModel
* Establishes the consumable -> admin user relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -152,7 +152,7 @@ class Consumable extends SnipeModel
* Establishes the component -> assignments relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumableAssignments()
@@ -164,7 +164,7 @@ class Consumable extends SnipeModel
* Establishes the component -> company relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -176,7 +176,7 @@ class Consumable extends SnipeModel
* Establishes the component -> manufacturer relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -188,7 +188,7 @@ class Consumable extends SnipeModel
* Establishes the component -> location relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -200,7 +200,7 @@ class Consumable extends SnipeModel
* Establishes the component -> category relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -213,7 +213,7 @@ class Consumable extends SnipeModel
* Establishes the component -> action logs relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -225,23 +225,28 @@ class Consumable extends SnipeModel
* Gets the full image url for the consumable
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return string | false
*/
public function getImageUrl()
{
// If there is a consumable image, use that
if ($this->image) {
return Storage::disk('public')->url(app('consumables_upload_path').$this->image);
}
return false;
// Otherwise check for a category image
} elseif (($this->category) && ($this->category->image)) {
return Storage::disk('public')->url(app('categories_upload_path').e($this->category->image));
}
return false;
}
/**
* Establishes the component -> users relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
*/
public function users() : Relation
{
@@ -252,7 +257,7 @@ class Consumable extends SnipeModel
* Establishes the item -> supplier relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1]
* @since [v6.1.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -266,7 +271,7 @@ class Consumable extends SnipeModel
* asset model category
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return bool
*/
public function checkin_email()
@@ -278,7 +283,7 @@ class Consumable extends SnipeModel
* Determine whether this asset requires acceptance by the assigned user
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return bool
*/
public function requireAcceptance()
@@ -291,7 +296,7 @@ class Consumable extends SnipeModel
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return string | false
*/
public function getEula()
@@ -309,7 +314,7 @@ class Consumable extends SnipeModel
* Check how many items within a consumable are checked out
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0]
* @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -321,7 +326,7 @@ class Consumable extends SnipeModel
* Checks the number of available consumables
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @since [v4.0]
* @return int
*/
public function numRemaining()
@@ -347,8 +352,8 @@ class Consumable extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4
* @param $value
* @since v6.3.4
* @param $value
* @return void
*/
public function setQtyAttribute($value)
@@ -365,8 +370,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -378,8 +383,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on location
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -391,8 +396,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on manufacturer
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -404,8 +409,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -417,8 +422,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on remaining
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -431,8 +436,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on supplier
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/

View File

@@ -16,6 +16,7 @@ class CustomField extends Model
UniqueUndeletedTrait;
/**
*
* Custom field predfined formats
*
* @var array
@@ -92,7 +93,7 @@ class CustomField extends Model
* table instead of the assets table.
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
* @since [v3.0]
*/
public static $table_name = 'assets';
@@ -103,7 +104,7 @@ class CustomField extends Model
* do with previously existing values. - @snipe
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return string
*/
public static function name_to_db_name($name)
@@ -120,66 +121,78 @@ class CustomField extends Model
* to do it in the controllers.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return bool
*/
public static function boot()
{
parent::boot();
self::created(function ($custom_field) {
self::created(
function ($custom_field) {
// Column already exists on the assets table - nothing to do here.
// This *shouldn't* happen in the wild.
if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
return false;
// Column already exists on the assets table - nothing to do here.
// This *shouldn't* happen in the wild.
if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
return false;
}
// Update the column name in the assets table
Schema::table(
self::$table_name, function ($table) use ($custom_field) {
$table->text($custom_field->convertUnicodeDbSlug())->nullable();
}
);
// Update the db_column property in the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
}
);
// Update the column name in the assets table
Schema::table(self::$table_name, function ($table) use ($custom_field) {
$table->text($custom_field->convertUnicodeDbSlug())->nullable();
});
self::updating(
function ($custom_field) {
// Update the db_column property in the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
});
// Column already exists on the assets table - nothing to do here.
if ($custom_field->isDirty('name')) {
if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
return true;
}
self::updating(function ($custom_field) {
// Rename the field if the name has changed
Schema::table(
self::$table_name, function ($table) use ($custom_field) {
$table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
}
);
// Save the updated column name to the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
// Column already exists on the assets table - nothing to do here.
if ($custom_field->isDirty('name')) {
if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
return true;
}
// Rename the field if the name has changed
Schema::table(self::$table_name, function ($table) use ($custom_field) {
$table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
});
// Save the updated column name to the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
return true;
}
return true;
});
);
// Drop the assets column if we've deleted it from custom fields
self::deleting(function ($custom_field) {
return Schema::table(self::$table_name, function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
});
self::deleting(
function ($custom_field) {
return Schema::table(
self::$table_name, function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
}
);
}
);
}
/**
* Establishes the customfield -> fieldset relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fieldset()
@@ -214,26 +227,26 @@ class CustomField extends Model
public function displayFieldInCurrentForm($form_type = null)
{
switch ($form_type) {
case 'audit':
return $this->displayFieldInAuditForm();
case 'checkin':
return $this->displayFieldInCheckinForm();
case 'checkout':
return $this->displayFieldInCheckoutForm();
case 'audit':
return $this->displayFieldInAuditForm();
case 'checkin':
return $this->displayFieldInCheckinForm();
case 'checkout':
return $this->displayFieldInCheckoutForm();
}
}
public function assetModels()
{
return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id');
return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id');
}
/**
* Establishes the customfield -> admin user relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -245,7 +258,7 @@ class CustomField extends Model
* Establishes the customfield -> default values relationship
*
* @author Hannah Tinkler
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
@@ -262,19 +275,23 @@ class CustomField extends Model
*/
public function defaultValue($modelId)
{
return $this->defaultValues->filter(function ($item) use ($modelId) {
return $item->pivot->asset_model_id == $modelId;
})->map(function ($item) {
return $item->pivot->default_value;
})->first();
return $this->defaultValues->filter(
function ($item) use ($modelId) {
return $item->pivot->asset_model_id == $modelId;
}
)->map(
function ($item) {
return $item->pivot->default_value;
}
)->first();
}
/**
* Checks the format of the attribute
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param $value string
* @since [v3.0]
* @param $value string
* @since [v3.0]
* @return bool
*/
public function check_format($value)
@@ -286,7 +303,7 @@ class CustomField extends Model
* Gets the DB column name.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return string
*/
public function db_column_name()
@@ -302,7 +319,7 @@ class CustomField extends Model
* user-friendly text in the dropdowns, and in the custom fields display.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return string
*/
public function getFormatAttribute($value)
@@ -320,7 +337,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return array
*/
public function setFormatAttribute($value)
@@ -336,7 +353,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return array
*/
public function formatFieldValuesAsArray()
@@ -366,7 +383,7 @@ class CustomField extends Model
* Check whether the field is encrypted
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return bool
*/
public function isFieldDecryptable($string)
@@ -383,7 +400,7 @@ class CustomField extends Model
* won't break the database.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @since [v3.4]
* @return string
*/
public function convertUnicodeDbSlug($original = null)
@@ -392,7 +409,7 @@ class CustomField extends Model
$id = $this->id ? $this->id : 'xx';
if (! function_exists('transliterator_transliterate')) {
$long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name),"UTF-8"), '_');
$long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name), "UTF-8"), '_');
} else {
$long_slug = '_snipeit_'.Utf8Slugger::slugify($name, '_');
}
@@ -402,9 +419,10 @@ class CustomField extends Model
/**
* Get validation rules for custom fields to use with Validator
*
* @author [V. Cordes] [<volker@fdatek.de>]
* @param int $id
* @since [v4.1.10]
* @param int $id
* @since [v4.1.10]
* @return array
*/
public function validationRules($regex_format = null)
@@ -418,6 +436,7 @@ class CustomField extends Model
/**
* Check to see if there is a custom regex format type
*
* @see https://github.com/grokability/snipe-it/issues/5896
*
* @author Wes Hulette <jwhulette@gmail.com>

View File

@@ -3,12 +3,19 @@
namespace App\Models;
use App\Rules\AlphaEncrypted;
use App\Rules\BooleanEncrypted;
use App\Rules\DateEncrypted;
use App\Rules\EmailEncrypted;
use App\Rules\IPEncrypted;
use App\Rules\IPv4Encrypted;
use App\Rules\IPv6Encrypted;
use App\Rules\MacEncrypted;
use App\Rules\NumericEncrypted;
use App\Rules\RegexEncrypted;
use App\Rules\UrlEncrypted;
use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model
@@ -20,6 +27,7 @@ class CustomFieldset extends Model
/**
* Validation rules
*
* @var array
*/
public $rules = [
@@ -39,7 +47,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> field relationship
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fields()
@@ -51,7 +59,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> models relationship
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -63,7 +71,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> admin user relationship
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -76,14 +84,14 @@ class CustomFieldset extends Model
if ($this->fields) {
switch ($form_type) {
case 'audit':
return $this->fields->where('display_audit', '1')->count() > 0;
case 'checkin':
return $this->fields->where('display_checkin', '1')->count() > 0;
case 'checkout':
return $this->fields->where('display_checkout', '1')->count() > 0;
default:
return true;
case 'audit':
return $this->fields->where('display_audit', '1')->count() > 0;
case 'checkin':
return $this->fields->where('display_checkin', '1')->count() > 0;
case 'checkout':
return $this->fields->where('display_checkout', '1')->count() > 0;
default:
return true;
}
}
@@ -95,17 +103,18 @@ class CustomFieldset extends Model
* custom field format
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @since [v3.0]
* @return array
*/
public function validation_rules()
public function validation_rules(): array
{
$rules = [];
foreach ($this->fields as $field) {
$rule = [];
if (($field->field_encrypted != '1') ||
(($field->field_encrypted == '1') && (Gate::allows('admin')))) {
if (($field->field_encrypted != '1')
|| (($field->field_encrypted == '1') && (Gate::allows('admin')))
) {
$rule[] = ($field->pivot->required == '1') ? 'required' : 'nullable';
}
@@ -119,18 +128,65 @@ class CustomFieldset extends Model
$rules[$field->db_column_name()] = $rule;
// these are to replace the standard 'numeric' and 'alpha' rules if the custom field is also encrypted.
// the values need to be decrypted first, because encrypted strings are alphanumeric
if ($field->format === 'NUMERIC' && $field->field_encrypted) {
// this is to switch the rules to rules specially made for encrypted custom fields that decrypt the value before validating
if ($field->field_encrypted) {
$numericKey = array_search('numeric', $rules[$field->db_column_name()]);
$rules[$field->db_column_name()][$numericKey] = new NumericEncrypted;
$alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
$emailKey = array_search('email', $rules[$field->db_column_name()]);
$dateKey = array_search('date', $rules[$field->db_column_name()]);
$urlKey = array_search('url', $rules[$field->db_column_name()]);
$ipKey = array_search('ip', $rules[$field->db_column_name()]);
$ipv4Key = array_search('ipv4', $rules[$field->db_column_name()]);
$ipv6Key = array_search('ipv6', $rules[$field->db_column_name()]);
$macKey = array_search('regex:/^[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}$/', $rules[$field->db_column_name()]);
$booleanKey = array_search('boolean', $rules[$field->db_column_name()]);
// find objects in array that start with "regex:"
// collect because i couldn't figure how to do this
// with array filter and get keys out of it
$regexCollect = collect($rules[$field->db_column_name()]);
$regexKeys = $regexCollect->filter(function ($value, $key) {
return starts_with($value, 'regex:');
})->keys()->values()->toArray();
switch ($field->format) {
case 'NUMERIC':
$rules[$field->db_column_name()][$numericKey] = new NumericEncrypted;
break;
case 'ALPHA':
$rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
break;
case 'EMAIL':
$rules[$field->db_column_name()][$emailKey] = new EmailEncrypted;
break;
case 'DATE':
$rules[$field->db_column_name()][$dateKey] = new DateEncrypted;
break;
case 'URL':
$rules[$field->db_column_name()][$urlKey] = new UrlEncrypted;
break;
case 'IP':
$rules[$field->db_column_name()][$ipKey] = new IPEncrypted;
break;
case 'IPV4':
$rules[$field->db_column_name()][$ipv4Key] = new IPv4Encrypted;
break;
case 'IPV6':
$rules[$field->db_column_name()][$ipv6Key] = new IPv6Encrypted;
break;
case 'MAC':
$rules[$field->db_column_name()][$macKey] = new MacEncrypted;
break;
case 'BOOLEAN':
$rules[$field->db_column_name()][$booleanKey] = new BooleanEncrypted;
break;
case starts_with($field->format, 'regex'):
foreach ($regexKeys as $regexKey) {
$rules[$field->db_column_name()][$regexKey] = new RegexEncrypted;
}
break;
}
}
if ($field->format === 'ALPHA' && $field->field_encrypted) {
$alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
$rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
}
// add not_array to rules for all fields but checkboxes
if ($field->element != 'checkbox') {

View File

@@ -72,7 +72,7 @@ class Department extends SnipeModel
* Establishes the department -> company relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v4.0]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -84,7 +84,7 @@ class Department extends SnipeModel
* Establishes the department -> users relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v4.0]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
@@ -96,7 +96,7 @@ class Department extends SnipeModel
* Establishes the department -> manager relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v4.0]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manager()
@@ -108,7 +108,7 @@ class Department extends SnipeModel
* Establishes the department -> location relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v4.0]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -119,8 +119,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on location name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -132,8 +132,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on manager name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -145,8 +145,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on company
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/

View File

@@ -48,15 +48,15 @@ class Depreciable extends SnipeModel
$depreciation = 0;
$setting = Setting::getSettings();
switch ($setting->depreciation_method) {
case 'half_1':
case 'half_1':
$depreciation = $this->getHalfYearDepreciatedValue(true);
break;
case 'half_2':
case 'half_2':
$depreciation = $this->getHalfYearDepreciatedValue(false);
break;
default:
default:
$depreciation = $this->getLinearDepreciatedValue();
}
@@ -74,7 +74,7 @@ class Depreciable extends SnipeModel
return null;
}
if ($months_passed >= $this->get_depreciation()->months){
if ($months_passed >= $this->get_depreciation()->months) {
//if there is a floor use it
if($this->get_depreciation()->depreciation_min) {
@@ -93,14 +93,15 @@ class Depreciable extends SnipeModel
return $current_value;
}
public function getMonthlyDepreciation(){
public function getMonthlyDepreciation()
{
return ($this->purchase_cost-$this->calculateDepreciation())/$this->get_depreciation()->months;
}
/**
* @param onlyHalfFirstYear Boolean always applied only second half of the first year
* @param onlyHalfFirstYear Boolean always applied only second half of the first year
* @return float|int
*/
public function getHalfYearDepreciatedValue($onlyHalfFirstYear = false)
@@ -131,7 +132,7 @@ class Depreciable extends SnipeModel
}
/**
* @param \DateTime $date
* @param \DateTime $date
* @return int
*/
protected function get_fiscal_year($date)
@@ -146,7 +147,7 @@ class Depreciable extends SnipeModel
}
/**
* @param \DateTime $date
* @param \DateTime $date
* @return bool
*/
protected function is_first_half_of_year($date)

View File

@@ -56,7 +56,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> models relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v5.0]
* @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -68,7 +68,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> licenses relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v5.0]
* @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -80,7 +80,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> assets relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v5.0]
* @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -92,7 +92,7 @@ class Depreciation extends SnipeModel
* Get the user that created the depreciation
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.13]
* @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()

View File

@@ -51,7 +51,7 @@ class Group extends SnipeModel
* Establishes the groups -> users relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v1.0]
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
@@ -63,7 +63,7 @@ class Group extends SnipeModel
* Get the user that created the group
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.0]
* @since [v6.3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -75,7 +75,7 @@ class Group extends SnipeModel
* Decode JSON permissions into array
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v1.0]
* @since [v1.0]
* @return array | \stdClass
*/
public function decodePermissions()

View File

@@ -19,7 +19,7 @@ class Import extends Model
* Establishes the license -> admin user relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v2.0]
* @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()

View File

@@ -38,7 +38,8 @@ class DefaultLabel extends RectangleSheet
private int $rows;
public function __construct() {
public function __construct()
{
$settings = Setting::getSettings();
$this->textSize = Helper::convertUnit($settings->labels_fontsize, 'pt', 'in');
@@ -74,41 +75,116 @@ class DefaultLabel extends RectangleSheet
}
public function getUnit() { return 'in'; }
public function getUnit()
{
return 'in';
}
public function getPageWidth() { return $this->pageWidth; }
public function getPageHeight() { return $this->pageHeight; }
public function getPageWidth()
{
return $this->pageWidth;
}
public function getPageHeight()
{
return $this->pageHeight;
}
public function getPageMarginTop() { return $this->pageMarginTop; }
public function getPageMarginBottom() { return $this->pageMarginBottom; }
public function getPageMarginLeft() { return $this->pageMarginLeft; }
public function getPageMarginRight() { return $this->pageMarginRight; }
public function getPageMarginTop()
{
return $this->pageMarginTop;
}
public function getPageMarginBottom()
{
return $this->pageMarginBottom;
}
public function getPageMarginLeft()
{
return $this->pageMarginLeft;
}
public function getPageMarginRight()
{
return $this->pageMarginRight;
}
public function getColumns() { return $this->columns; }
public function getRows() { return $this->rows; }
public function getLabelBorder() { return 0; }
public function getColumns()
{
return $this->columns;
}
public function getRows()
{
return $this->rows;
}
public function getLabelBorder()
{
return 0;
}
public function getLabelWidth() { return $this->labelWidth; }
public function getLabelHeight() { return $this->labelHeight; }
public function getLabelWidth()
{
return $this->labelWidth;
}
public function getLabelHeight()
{
return $this->labelHeight;
}
public function getLabelMarginTop() { return 0; }
public function getLabelMarginBottom() { return 0; }
public function getLabelMarginLeft() { return 0; }
public function getLabelMarginRight() { return 0; }
public function getLabelMarginTop()
{
return 0;
}
public function getLabelMarginBottom()
{
return 0;
}
public function getLabelMarginLeft()
{
return 0;
}
public function getLabelMarginRight()
{
return 0;
}
public function getLabelColumnSpacing() { return $this->labelSpacingH; }
public function getLabelRowSpacing() { return $this->labelSpacingV; }
public function getLabelColumnSpacing()
{
return $this->labelSpacingH;
}
public function getLabelRowSpacing()
{
return $this->labelSpacingV;
}
public function getSupportAssetTag() { return false; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 4; }
public function getSupportTitle() { return true; }
public function getSupportLogo() { return true; }
public function getSupportAssetTag()
{
return false;
}
public function getSupport1DBarcode()
{
return true;
}
public function getSupport2DBarcode()
{
return true;
}
public function getSupportFields()
{
return 4;
}
public function getSupportTitle()
{
return true;
}
public function getSupportLogo()
{
return true;
}
public function preparePDF($pdf) {}
public function preparePDF($pdf)
{
}
public function write($pdf, $record) {
public function write($pdf, $record)
{
$asset = $record->get('asset');
$settings = Setting::getSettings();

View File

@@ -5,21 +5,30 @@ namespace App\Models\Labels;
use App\Models\Asset;
use Illuminate\Support\Collection;
class Field {
class Field
{
protected Collection $options;
public function getOptions() { return $this->options; }
public function setOptions($options) {
public function getOptions()
{
return $this->options;
}
public function setOptions($options)
{
$tempCollect = collect($options);
if (!$tempCollect->contains(fn($o) => !is_subclass_of($o, FieldOption::class))) {
$this->options = $options;
}
}
public function toArray(Asset $asset) { return Field::makeArray($this, $asset); }
public function toArray(Asset $asset)
{
return Field::makeArray($this, $asset);
}
/* Statics */
public static function makeArray(Field $field, Asset $asset) {
public static function makeArray(Field $field, Asset $asset)
{
return $field->getOptions()
// filter out any FieldOptions that are accidentally null
->filter()
@@ -27,11 +36,13 @@ class Field {
->filter(fn($result) => $result['value'] != null);
}
public static function makeString(Field $option) {
public static function makeString(Field $option)
{
return implode('|', $option->getOptions());
}
public static function fromString(string $theString) {
public static function fromString(string $theString)
{
$field = new Field();
$field->options = collect(explode('|', $theString))
->filter(fn($optionString) => !empty($optionString))

View File

@@ -5,14 +5,22 @@ namespace App\Models\Labels;
use App\Models\Asset;
use Illuminate\Support\Collection;
class FieldOption {
class FieldOption
{
protected string $label;
public function getLabel() { return $this->label; }
public function getLabel()
{
return $this->label;
}
protected string $dataSource;
public function getDataSource() { return $this->dataSource; }
public function getDataSource()
{
return $this->dataSource;
}
public function getValue(Asset $asset) {
public function getValue(Asset $asset)
{
$dataPath = collect(explode('.', $this->dataSource));
// assignedTo directly on the asset is a special case where
@@ -33,18 +41,29 @@ class FieldOption {
return $asset->purchase_date ? $asset->purchase_date->format('Y-m-d') : null;
}
return $dataPath->reduce(function ($myValue, $path) {
try { return $myValue ? $myValue->{$path} : ${$myValue}; }
catch (\Exception $e) { return $myValue; }
}, $asset);
return $dataPath->reduce(
function ($myValue, $path) {
try { return $myValue ? $myValue->{$path} : ${$myValue};
}
catch (\Exception $e) { return $myValue;
}
}, $asset
);
}
public function toArray(Asset $asset=null) { return FieldOption::makeArray($this, $asset); }
public function toString() { return FieldOption::makeString($this); }
public function toArray(Asset $asset=null)
{
return FieldOption::makeArray($this, $asset);
}
public function toString()
{
return FieldOption::makeString($this);
}
/* Statics */
public static function makeArray(FieldOption $option, Asset $asset=null) {
public static function makeArray(FieldOption $option, Asset $asset=null)
{
return [
'label' => $option->getLabel(),
'dataSource' => $option->getDataSource(),
@@ -52,11 +71,13 @@ class FieldOption {
];
}
public static function makeString(FieldOption $option) {
public static function makeString(FieldOption $option)
{
return $option->getLabel() . '=' . $option->getDataSource();
}
public static function fromString(string $theString) {
public static function fromString(string $theString)
{
$parts = explode('=', $theString);
if (count($parts) == 2) {
$option = new FieldOption();

View File

@@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Log;
/**
* Model for Labels.
*
* @version v1.0
* @version v1.0
*/
abstract class Label
{
@@ -32,7 +32,8 @@ abstract class Label
*
* @return int
*/
public function getRotation() {
public function getRotation()
{
return 0;
}
@@ -123,29 +124,32 @@ abstract class Label
/**
* Make changes to the PDF properties here. OPTIONAL.
*
* @param TCPDF $pdf The TCPDF instance
* @param TCPDF $pdf The TCPDF instance
*/
public abstract function preparePDF(TCPDF $pdf);
/**
* Write single data record as content here.
*
* @param TCPDF $pdf The TCPDF instance
* @param Collection $record A data record
* @param TCPDF $pdf The TCPDF instance
* @param Collection $record A data record
*/
public abstract function write(TCPDF $pdf, Collection $record);
/**
* Handle the data here. Override for multiple-per-page handling
*
* @param TCPDF $pdf The TCPDF instance
* @param Collection $data The data
* @param TCPDF $pdf The TCPDF instance
* @param Collection $data The data
*/
public function writeAll(TCPDF $pdf, Collection $data) {
$data->each(function ($record, $index) use ($pdf) {
$pdf->AddPage();
$this->write($pdf, $record);
});
public function writeAll(TCPDF $pdf, Collection $data)
{
$data->each(
function ($record, $index) use ($pdf) {
$pdf->AddPage();
$this->write($pdf, $record);
}
);
}
/**
@@ -153,7 +157,8 @@ abstract class Label
*
* @return string
*/
public final function getName() {
public final function getName()
{
$refClass = new \ReflectionClass(Label::class);
return str_replace($refClass->getNamespaceName() . '\\', '', get_class($this));
}
@@ -165,7 +170,8 @@ abstract class Label
*
* @return string
*/
public final function getOrientation() {
public final function getOrientation()
{
return ($this->getWidth() >= $this->getHeight()) ? 'L' : 'P';
}
@@ -174,7 +180,8 @@ abstract class Label
*
* @return object [ 'x1'=>0.00, 'y1'=>0.00, 'x2'=>0.00, 'y2'=>0.00, 'w'=>0.00, 'h'=>0.00 ]
*/
public final function getPrintableArea() {
public final function getPrintableArea()
{
return (object)[
'x1' => $this->getMarginLeft(),
'y1' => $this->getMarginTop(),
@@ -188,21 +195,22 @@ abstract class Label
/**
* Write a text cell.
*
* @param TCPDF $pdf The TCPDF instance
* @param string $text The text to write. Supports 'some **bold** text'.
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param string $font The font family
* @param string $style The font style
* @param int $size The font size in getUnit() units
* @param string $align Align text in the box. 'L' left, 'R' right, 'C' center.
* @param float $width Force text box width. NULL to auto-fit.
* @param float $height Force text box height. NULL to auto-fit.
* @param bool $squash Squash text if it's too big
* @param int $border Thickness of border. Default = 0.
* @param int $spacing Letter spacing. Default = 0.
* @param TCPDF $pdf The TCPDF instance
* @param string $text The text to write. Supports 'some **bold** text'.
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param string $font The font family
* @param string $style The font style
* @param int $size The font size in getUnit() units
* @param string $align Align text in the box. 'L' left, 'R' right, 'C' center.
* @param float $width Force text box width. NULL to auto-fit.
* @param float $height Force text box height. NULL to auto-fit.
* @param bool $squash Squash text if it's too big
* @param int $border Thickness of border. Default = 0.
* @param int $spacing Letter spacing. Default = 0.
*/
public final function writeText(TCPDF $pdf, $text, $x, $y, $font=null, $style=null, $size=null, $align='L', $width=null, $height=null, $squash=false, $border=0, $spacing=0) {
public final function writeText(TCPDF $pdf, $text, $x, $y, $font=null, $style=null, $size=null, $align='L', $width=null, $height=null, $squash=false, $border=0, $spacing=0)
{
$prevFamily = $pdf->getFontFamily();
$prevStyle = $pdf->getFontStyle();
$prevSizePt = $pdf->getFontSizePt();
@@ -211,33 +219,42 @@ abstract class Label
$fontFamily = !empty($font) ? $font : $prevFamily;
$fontStyle = !empty($style) ? $style : $prevStyle;
if ($size) $fontSizePt = Helper::convertUnit($size, $this->getUnit(), 'pt', true);
else $fontSizePt = $prevSizePt;
if ($size) { $fontSizePt = Helper::convertUnit($size, $this->getUnit(), 'pt', true);
} else { $fontSizePt = $prevSizePt;
}
$pdf->SetFontSpacing($spacing);
$parts = collect(explode('**', $text))
->map(function ($part, $index) use ($pdf, $fontFamily, $fontStyle, $fontSizePt) {
$modStyle = ($index % 2 == 1) ? 'B' : $fontStyle;
$pdf->setFont($fontFamily, $modStyle, $fontSizePt);
return [
->map(
function ($part, $index) use ($pdf, $fontFamily, $fontStyle, $fontSizePt) {
$modStyle = ($index % 2 == 1) ? 'B' : $fontStyle;
$pdf->setFont($fontFamily, $modStyle, $fontSizePt);
return [
'text' => $part,
'text_width' => $pdf->GetStringWidth($part),
'font_family' => $fontFamily,
'font_style' => $modStyle,
'font_size' => $fontSizePt,
];
});
];
}
);
$textWidth = $parts->reduce(function ($carry, $part) { return $carry += $part['text_width']; });
$textWidth = $parts->reduce(
function ($carry, $part) {
return $carry += $part['text_width'];
}
);
$cellWidth = !empty($width) ? $width : $textWidth;
if ($squash && ($textWidth > 0)) {
$scaleFactor = min(1.0, $cellWidth / $textWidth);
$parts = $parts->map(function ($part, $index) use ($scaleFactor) {
$part['text_width'] = $part['text_width'] * $scaleFactor;
return $part;
});
$parts = $parts->map(
function ($part, $index) use ($scaleFactor) {
$part['text_width'] = $part['text_width'] * $scaleFactor;
return $part;
}
);
}
$cellHeight = !empty($height) ? $height : Helper::convertUnit($fontSizePt, 'pt', $this->getUnit());
@@ -249,18 +266,23 @@ abstract class Label
}
switch($align) {
case 'R': $startX = ($x + $cellWidth) - min($cellWidth, $textWidth); break;
case 'C': $startX = ($x + ($cellWidth / 2)) - (min($cellWidth, $textWidth) / 2); break;
case 'L':
default: $startX = $x; break;
case 'R': $startX = ($x + $cellWidth) - min($cellWidth, $textWidth);
break;
case 'C': $startX = ($x + ($cellWidth / 2)) - (min($cellWidth, $textWidth) / 2);
break;
case 'L':
default: $startX = $x;
break;
}
$parts->reduce(function ($currentX, $part) use ($pdf, $y, $cellHeight) {
$pdf->SetXY($currentX, $y);
$pdf->setFont($part['font_family'], $part['font_style'], $part['font_size']);
$pdf->Cell($part['text_width'], $cellHeight, $part['text'], 0, 0, '', false, '', 1, true);
return $currentX += $part['text_width'];
}, $startX);
$parts->reduce(
function ($currentX, $part) use ($pdf, $y, $cellHeight) {
$pdf->SetXY($currentX, $y);
$pdf->setFont($part['font_family'], $part['font_style'], $part['font_size']);
$pdf->Cell($part['text_width'], $cellHeight, $part['text'], 0, 0, '', false, '', 1, true);
return $currentX += $part['text_width'];
}, $startX
);
$pdf->SetFont($prevFamily, $prevStyle, $prevSizePt);
$pdf->SetFontSpacing(0);
@@ -269,27 +291,30 @@ abstract class Label
/**
* Write an image.
*
* @param TCPDF $pdf The TCPDF instance
* @param string $image The image to write
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
* @param string $halign Align text in the box. 'L' left, 'R' right, 'C' center. Default 'L'.
* @param string $valign Align text in the box. 'T' top, 'B' bottom, 'C' center. Default 'T'.
* @param int $dpi Pixels per inch
* @param bool $resize Resize to fit container
* @param bool $stretch Stretch (vs Scale) to fit container
* @param int $border Thickness of border. Default = 0.
* @param TCPDF $pdf The TCPDF instance
* @param string $image The image to write
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
* @param string $halign Align text in the box. 'L' left, 'R' right, 'C' center. Default 'L'.
* @param string $valign Align text in the box. 'T' top, 'B' bottom, 'C' center. Default 'T'.
* @param int $dpi Pixels per inch
* @param bool $resize Resize to fit container
* @param bool $stretch Stretch (vs Scale) to fit container
* @param int $border Thickness of border. Default = 0.
*
* @return array Returns the final calculated size [w,h]
*/
public final function writeImage(TCPDF $pdf, $image, $x, $y, $width=null, $height=null, $halign='L', $valign='L', $dpi=300, $resize=false, $stretch=false, $border=0) {
public final function writeImage(TCPDF $pdf, $image, $x, $y, $width=null, $height=null, $halign='L', $valign='L', $dpi=300, $resize=false, $stretch=false, $border=0)
{
if (empty($image)) return [0,0];
if (empty($image)) { return [0,0];
}
$imageInfo = getimagesize($image);
if (!$imageInfo) return [0,0]; // TODO: SVG or other
if (!$imageInfo) { return [0,0]; // TODO: SVG or other
}
$imageWidthPx = $imageInfo[0];
$imageHeightPx = $imageInfo[1];
@@ -342,18 +367,24 @@ abstract class Label
// Horizontal Position
switch ($halign) {
case 'R': $originX = ($x + $containerWidth) - $outputWidth; break;
case 'C': $originX = ($x + ($containerWidth / 2)) - ($outputWidth / 2); break;
case 'L':
default: $originX = $x; break;
case 'R': $originX = ($x + $containerWidth) - $outputWidth;
break;
case 'C': $originX = ($x + ($containerWidth / 2)) - ($outputWidth / 2);
break;
case 'L':
default: $originX = $x;
break;
}
// Vertical Position
switch ($valign) {
case 'B': $originY = ($y + $containerHeight) - $outputHeight; break;
case 'C': $originY = ($y + ($containerHeight / 2)) - ($outputHeight / 2); break;
case 'T':
default: $originY = $y; break;
case 'B': $originY = ($y + $containerHeight) - $outputHeight;
break;
case 'C': $originY = ($y + ($containerHeight / 2)) - ($outputHeight / 2);
break;
case 'T':
default: $originY = $y;
break;
}
// Actual Image
@@ -373,16 +404,18 @@ abstract class Label
/**
* Write a 1D barcode.
*
* @param TCPDF $pdf The TCPDF instance
* @param string $value The barcode content
* @param string $type The barcode type
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
* @param TCPDF $pdf The TCPDF instance
* @param string $value The barcode content
* @param string $type The barcode type
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
*/
public final function write1DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height) {
if (empty($value)) return;
public final function write1DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height)
{
if (empty($value)) { return;
}
try {
$pdf->write1DBarcode($value, $type, $x, $y, $width, $height, null, ['stretch'=>true]);
} catch (\Exception|TypeError $e) {
@@ -393,16 +426,18 @@ abstract class Label
/**
* Write a 2D barcode.
*
* @param TCPDF $pdf The TCPDF instance
* @param string $value The barcode content
* @param string $type The barcode type
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
* @param TCPDF $pdf The TCPDF instance
* @param string $value The barcode content
* @param string $type The barcode type
* @param float $x X position of top-left
* @param float $y Y position of top-left
* @param float $width The container width
* @param float $height The container height
*/
public final function write2DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height) {
if (empty($value)) return;
public final function write2DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height)
{
if (empty($value)) { return;
}
$pdf->write2DBarcode($value, $type, $x, $y, $width, $height, null, ['stretch'=>true]);
}
@@ -411,127 +446,180 @@ abstract class Label
/**
* Checks the template is internally valid
*/
public final function validate() : void {
public final function validate() : void
{
$this->validateUnits();
$this->validateSize();
$this->validateMargins();
$this->validateSupport();
}
private function validateUnits() : void {
private function validateUnits() : void
{
$validUnits = [ 'pt', 'mm', 'cm', 'in' ];
$unit = $this->getUnit();
if (!in_array(strtolower($unit), $validUnits)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_value', [
'name' => 'getUnit()',
'expected' => '[ \''.implode('\', \'', $validUnits).'\' ]',
'actual' => '\''.$unit.'\''
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_value', [
'name' => 'getUnit()',
'expected' => '[ \''.implode('\', \'', $validUnits).'\' ]',
'actual' => '\''.$unit.'\''
]
)
);
}
}
private function validateSize() : void {
private function validateSize() : void
{
$width = $this->getWidth();
if (!is_numeric($width) || is_string($width)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getWidth()',
'expected' => 'float',
'actual' => gettype($width)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getWidth()',
'expected' => 'float',
'actual' => gettype($width)
]
)
);
}
$height = $this->getHeight();
if (!is_numeric($height) || is_string($height)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getHeight()',
'expected' => 'float',
'actual' => gettype($height)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getHeight()',
'expected' => 'float',
'actual' => gettype($height)
]
)
);
}
}
private function validateMargins() : void {
private function validateMargins() : void
{
$marginTop = $this->getMarginTop();
if (!is_numeric($marginTop) || is_string($marginTop)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getMarginTop()',
'expected' => 'float',
'actual' => gettype($marginTop)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getMarginTop()',
'expected' => 'float',
'actual' => gettype($marginTop)
]
)
);
}
$marginBottom = $this->getMarginBottom();
if (!is_numeric($marginBottom) || is_string($marginBottom)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getMarginBottom()',
'expected' => 'float',
'actual' => gettype($marginBottom)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getMarginBottom()',
'expected' => 'float',
'actual' => gettype($marginBottom)
]
)
);
}
$marginLeft = $this->getMarginLeft();
if (!is_numeric($marginLeft) || is_string($marginLeft)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getMarginLeft()',
'expected' => 'float',
'actual' => gettype($marginLeft)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getMarginLeft()',
'expected' => 'float',
'actual' => gettype($marginLeft)
]
)
);
}
$marginRight = $this->getMarginRight();
if (!is_numeric($marginRight) || is_string($marginRight)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getMarginRight()',
'expected' => 'float',
'actual' => gettype($marginRight)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getMarginRight()',
'expected' => 'float',
'actual' => gettype($marginRight)
]
)
);
}
}
private function validateSupport() : void {
private function validateSupport() : void
{
$support1D = $this->getSupport1DBarcode();
if (!is_bool($support1D)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getSupport1DBarcode()',
'expected' => 'boolean',
'actual' => gettype($support1D)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getSupport1DBarcode()',
'expected' => 'boolean',
'actual' => gettype($support1D)
]
)
);
}
$support2D = $this->getSupport2DBarcode();
if (!is_bool($support2D)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getSupport2DBarcode()',
'expected' => 'boolean',
'actual' => gettype($support2D)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getSupport2DBarcode()',
'expected' => 'boolean',
'actual' => gettype($support2D)
]
)
);
}
$supportFields = $this->getSupportFields();
if (!is_int($supportFields)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getSupportFields()',
'expected' => 'integer',
'actual' => gettype($supportFields)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getSupportFields()',
'expected' => 'integer',
'actual' => gettype($supportFields)
]
)
);
}
$supportLogo = $this->getSupportLogo();
if (!is_bool($supportLogo)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getSupportLogo()',
'expected' => 'boolean',
'actual' => gettype($supportLogo)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getSupportLogo()',
'expected' => 'boolean',
'actual' => gettype($supportLogo)
]
)
);
}
$supportTitle = $this->getSupportTitle();
if (!is_bool($supportTitle)) {
throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
'name' => 'getSupportTitle()',
'expected' => 'boolean',
'actual' => gettype($supportTitle)
]));
throw new \UnexpectedValueException(
trans(
'admin/labels/message.invalid_return_type', [
'name' => 'getSupportTitle()',
'expected' => 'boolean',
'actual' => gettype($supportTitle)
]
)
);
}
}
@@ -539,23 +627,26 @@ abstract class Label
/**
* Public Static Functions
*/
*/
/**
* Find size of a page by its format.
*
* @param string $format Format name (eg: 'A4', 'LETTER', etc.)
* @param string $orientation 'L' for Landscape, 'P' for Portrait ('L' default)
* @param string $unit Unit of measure to return in ('mm' default)
* @param string $format Format name (eg: 'A4', 'LETTER', etc.)
* @param string $orientation 'L' for Landscape, 'P' for Portrait ('L' default)
* @param string $unit Unit of measure to return in ('mm' default)
*
* @return object (object)[ 'width' => (float)123.4, 'height' => (float)123.4 ]
*/
public static function fromFormat($format, $orientation='L', $unit='mm', $round=false) {
public static function fromFormat($format, $orientation='L', $unit='mm', $round=false)
{
$size = collect(TCPDF_STATIC::getPageSizeFromFormat(strtoupper($format)))
->sort()
->map(function ($value) use ($unit) {
return Helper::convertUnit($value, 'pt', $unit);
})
->map(
function ($value) use ($unit) {
return Helper::convertUnit($value, 'pt', $unit);
}
)
->toArray();
$width = ($orientation == 'L') ? $size[1] : $size[0];
$height = ($orientation == 'L') ? $size[0] : $size[1];
@@ -571,16 +662,19 @@ abstract class Label
* Unlike most Models, these are defined by their existence as non-
* abstract classes stored in Models\Labels.
*
* @param string|Arrayable|array|null $path Label path[s]
* @param string|Arrayable|array|null $path Label path[s]
* @return Collection|Label|null
*/
public static function find($name=null) {
public static function find($name=null)
{
// Find many
if (is_array($name) || $name instanceof Arrayable) {
$labels = collect($name)
->map(function ($thisname) {
return static::find($thisname);
})
->map(
function ($thisname) {
return static::find($thisname);
}
)
->whereNotNull();
return ($labels->count() > 0) ? $labels : null;
}
@@ -588,26 +682,36 @@ abstract class Label
// Find one
if ($name !== null) {
return static::find()
->sole(function ($label) use ($name) {
return $label->getName() == $name;
});
->sole(
function ($label) use ($name) {
return $label->getName() == $name;
}
);
}
// Find all
return collect(File::allFiles(__DIR__))
->map(function ($file) {
preg_match_all('/\/*(.+?)(?:\/|\.)/', $file->getRelativePathName(), $matches);
return __NAMESPACE__ . '\\' . implode('\\', $matches[1]);
})
->filter(function ($name) {
if (!class_exists($name)) return false;
$refClass = new \ReflectionClass($name);
if ($refClass->isAbstract()) return false;
return $refClass->isSubclassOf(Label::class);
})
->map(function ($name) {
return new $name();
});
->map(
function ($file) {
preg_match_all('/\/*(.+?)(?:\/|\.)/', $file->getRelativePathName(), $matches);
return __NAMESPACE__ . '\\' . implode('\\', $matches[1]);
}
)
->filter(
function ($name) {
if (!class_exists($name)) { return false;
}
$refClass = new \ReflectionClass($name);
if ($refClass->isAbstract()) { return false;
}
return $refClass->isSubclassOf(Label::class);
}
)
->map(
function ($name) {
return new $name();
}
);
}

View File

@@ -33,9 +33,13 @@ abstract class RectangleSheet extends Sheet
public abstract function getLabelRowSpacing();
public function getLabelsPerPage() { return $this->getColumns() * $this->getRows(); }
public function getLabelsPerPage()
{
return $this->getColumns() * $this->getRows();
}
public function getLabelPosition($index) {
public function getLabelPosition($index)
{
$printIndex = $index + $this->getLabelIndexOffset();
$row = (int)($printIndex / $this->getColumns());
$col = $printIndex - ($row * $this->getColumns());

View File

@@ -6,12 +6,30 @@ abstract class Sheet extends Label
{
protected int $indexOffset = 0;
public function getWidth() { return $this->getPageWidth(); }
public function getHeight() { return $this->getPageHeight(); }
public function getMarginTop() { return $this->getPageMarginTop(); }
public function getMarginBottom() { return $this->getPageMarginBottom(); }
public function getMarginLeft() { return $this->getPageMarginLeft(); }
public function getMarginRight() { return $this->getPageMarginRight(); }
public function getWidth()
{
return $this->getPageWidth();
}
public function getHeight()
{
return $this->getPageHeight();
}
public function getMarginTop()
{
return $this->getPageMarginTop();
}
public function getMarginBottom()
{
return $this->getPageMarginBottom();
}
public function getMarginLeft()
{
return $this->getPageMarginLeft();
}
public function getMarginRight()
{
return $this->getPageMarginRight();
}
/**
* Returns the page width in getUnit() units
@@ -107,7 +125,7 @@ abstract class Sheet extends Label
/**
* Returns label position based on its index
*
* @param int $index
* @param int $index
*
* @return array [x,y]
*/
@@ -123,10 +141,11 @@ abstract class Sheet extends Label
/**
* Handle the data here. Override for multiple-per-page handling
*
* @param TCPDF $pdf The TCPDF instance
* @param Collection $data The data
* @param TCPDF $pdf The TCPDF instance
* @param Collection $data The data
*/
public function writeAll($pdf, $data) {
public function writeAll($pdf, $data)
{
$prevPageNumber = -1;
foreach ($data->toArray() as $recordIndex => $record) {
@@ -170,7 +189,8 @@ abstract class Sheet extends Label
*
* @return string
*/
public final function getLabelOrientation() {
public final function getLabelOrientation()
{
return ($this->getLabelWidth() >= $this->getLabelHeight()) ? 'L' : 'P';
}
@@ -179,7 +199,8 @@ abstract class Sheet extends Label
*
* @return object [ 'x1'=>0.00, 'y1'=>0.00, 'x2'=>0.00, 'y2'=>0.00, 'w'=>0.00, 'h'=>0.00 ]
*/
public final function getLabelPrintableArea() {
public final function getLabelPrintableArea()
{
return (object)[
'x1' => $this->getLabelMarginLeft(),
'y1' => $this->getLabelMarginTop(),
@@ -195,15 +216,20 @@ abstract class Sheet extends Label
*
* @return int
*/
public function getLabelIndexOffset() { return $this->indexOffset; }
public function getLabelIndexOffset()
{
return $this->indexOffset;
}
/**
* Sets label index offset (skip positions)
*
* @param int $offset
*
* @param int $offset
*/
public function setLabelIndexOffset(int $offset) { $this->indexOffset = $offset; }
public function setLabelIndexOffset(int $offset)
{
$this->indexOffset = $offset;
}
}
?>

View File

@@ -31,7 +31,8 @@ abstract class L7162 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
public function __construct() {
public function __construct()
{
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 0);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class L7162 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
public function getPageWidth() { return $this->pageWidth; }
public function getPageHeight() { return $this->pageHeight; }
public function getPageWidth()
{
return $this->pageWidth;
}
public function getPageHeight()
{
return $this->pageHeight;
}
public function getPageMarginTop() { return $this->pageMarginTop; }
public function getPageMarginBottom() { return $this->pageMarginTop; }
public function getPageMarginLeft() { return $this->pageMarginLeft; }
public function getPageMarginRight() { return $this->pageMarginLeft; }
public function getPageMarginTop()
{
return $this->pageMarginTop;
}
public function getPageMarginBottom()
{
return $this->pageMarginTop;
}
public function getPageMarginLeft()
{
return $this->pageMarginLeft;
}
public function getPageMarginRight()
{
return $this->pageMarginLeft;
}
public function getColumns() { return 2; }
public function getRows() { return 8; }
public function getColumns()
{
return 2;
}
public function getRows()
{
return 8;
}
public function getLabelColumnSpacing() { return $this->columnSpacing; }
public function getLabelRowSpacing() { return $this->rowSpacing; }
public function getLabelColumnSpacing()
{
return $this->columnSpacing;
}
public function getLabelRowSpacing()
{
return $this->rowSpacing;
}
public function getLabelWidth() { return $this->labelWidth; }
public function getLabelHeight() { return $this->labelHeight; }
public function getLabelWidth()
{
return $this->labelWidth;
}
public function getLabelHeight()
{
return $this->labelHeight;
}
public function getLabelBorder() { return 0; }
public function getLabelBorder()
{
return 0;
}
}
?>

View File

@@ -14,23 +14,59 @@ class L7162_A extends L7162
private const FIELD_SIZE = 4.60;
private const FIELD_MARGIN = 0.30;
public function getUnit() { return 'mm'; }
public function getUnit()
{
return 'mm';
}
public function getLabelMarginTop() { return 1.0; }
public function getLabelMarginBottom() { return 1.0; }
public function getLabelMarginLeft() { return 1.0; }
public function getLabelMarginRight() { return 1.0; }
public function getLabelMarginTop()
{
return 1.0;
}
public function getLabelMarginBottom()
{
return 1.0;
}
public function getLabelMarginLeft()
{
return 1.0;
}
public function getLabelMarginRight()
{
return 1.0;
}
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return false; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 4; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function getSupportAssetTag()
{
return true;
}
public function getSupport1DBarcode()
{
return false;
}
public function getSupport2DBarcode()
{
return true;
}
public function getSupportFields()
{
return 4;
}
public function getSupportLogo()
{
return false;
}
public function getSupportTitle()
{
return true;
}
public function preparePDF($pdf) {}
public function preparePDF($pdf)
{
}
public function write($pdf, $record) {
public function write($pdf, $record)
{
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;

View File

@@ -17,23 +17,59 @@ class L7162_B extends L7162
private const FIELD_SIZE = 4.20;
private const FIELD_MARGIN = 0.30;
public function getUnit() { return 'mm'; }
public function getUnit()
{
return 'mm';
}
public function getLabelMarginTop() { return 1.0; }
public function getLabelMarginBottom() { return 0; }
public function getLabelMarginLeft() { return 1.0; }
public function getLabelMarginRight() { return 1.0; }
public function getLabelMarginTop()
{
return 1.0;
}
public function getLabelMarginBottom()
{
return 0;
}
public function getLabelMarginLeft()
{
return 1.0;
}
public function getLabelMarginRight()
{
return 1.0;
}
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return false; }
public function getSupportFields() { return 3; }
public function getSupportLogo() { return true; }
public function getSupportTitle() { return true; }
public function getSupportAssetTag()
{
return true;
}
public function getSupport1DBarcode()
{
return true;
}
public function getSupport2DBarcode()
{
return false;
}
public function getSupportFields()
{
return 3;
}
public function getSupportLogo()
{
return true;
}
public function getSupportTitle()
{
return true;
}
public function preparePDF($pdf) {}
public function preparePDF($pdf)
{
}
public function write($pdf, $record) {
public function write($pdf, $record)
{
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;

View File

@@ -31,7 +31,8 @@ abstract class L7163 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
public function __construct() {
public function __construct()
{
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 0);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class L7163 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
public function getPageWidth() { return $this->pageWidth; }
public function getPageHeight() { return $this->pageHeight; }
public function getPageWidth()
{
return $this->pageWidth;
}
public function getPageHeight()
{
return $this->pageHeight;
}
public function getPageMarginTop() { return $this->pageMarginTop; }
public function getPageMarginBottom() { return $this->pageMarginTop; }
public function getPageMarginLeft() { return $this->pageMarginLeft; }
public function getPageMarginRight() { return $this->pageMarginLeft; }
public function getPageMarginTop()
{
return $this->pageMarginTop;
}
public function getPageMarginBottom()
{
return $this->pageMarginTop;
}
public function getPageMarginLeft()
{
return $this->pageMarginLeft;
}
public function getPageMarginRight()
{
return $this->pageMarginLeft;
}
public function getColumns() { return 2; }
public function getRows() { return 7; }
public function getColumns()
{
return 2;
}
public function getRows()
{
return 7;
}
public function getLabelColumnSpacing() { return $this->columnSpacing; }
public function getLabelRowSpacing() { return $this->rowSpacing; }
public function getLabelColumnSpacing()
{
return $this->columnSpacing;
}
public function getLabelRowSpacing()
{
return $this->rowSpacing;
}
public function getLabelWidth() { return $this->labelWidth; }
public function getLabelHeight() { return $this->labelHeight; }
public function getLabelWidth()
{
return $this->labelWidth;
}
public function getLabelHeight()
{
return $this->labelHeight;
}
public function getLabelBorder() { return 0; }
public function getLabelBorder()
{
return 0;
}
}
?>

View File

@@ -14,23 +14,59 @@ class L7163_A extends L7163
private const FIELD_SIZE = 4.80;
private const FIELD_MARGIN = 0.30;
public function getUnit() { return 'mm'; }
public function getUnit()
{
return 'mm';
}
public function getLabelMarginTop() { return 1.0; }
public function getLabelMarginBottom() { return 1.0; }
public function getLabelMarginLeft() { return 1.0; }
public function getLabelMarginRight() { return 1.0; }
public function getLabelMarginTop()
{
return 1.0;
}
public function getLabelMarginBottom()
{
return 1.0;
}
public function getLabelMarginLeft()
{
return 1.0;
}
public function getLabelMarginRight()
{
return 1.0;
}
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return false; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 4; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function getSupportAssetTag()
{
return true;
}
public function getSupport1DBarcode()
{
return false;
}
public function getSupport2DBarcode()
{
return true;
}
public function getSupportFields()
{
return 4;
}
public function getSupportLogo()
{
return false;
}
public function getSupportTitle()
{
return true;
}
public function preparePDF($pdf) {}
public function preparePDF($pdf)
{
}
public function write($pdf, $record) {
public function write($pdf, $record)
{
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;

View File

@@ -31,7 +31,8 @@ abstract class _3490 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
public function __construct() {
public function __construct()
{
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 2);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class _3490 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
public function getPageWidth() { return $this->pageWidth; }
public function getPageHeight() { return $this->pageHeight; }
public function getPageWidth()
{
return $this->pageWidth;
}
public function getPageHeight()
{
return $this->pageHeight;
}
public function getPageMarginTop() { return $this->pageMarginTop; }
public function getPageMarginBottom() { return $this->pageMarginTop; }
public function getPageMarginLeft() { return $this->pageMarginLeft; }
public function getPageMarginRight() { return $this->pageMarginLeft; }
public function getPageMarginTop()
{
return $this->pageMarginTop;
}
public function getPageMarginBottom()
{
return $this->pageMarginTop;
}
public function getPageMarginLeft()
{
return $this->pageMarginLeft;
}
public function getPageMarginRight()
{
return $this->pageMarginLeft;
}
public function getColumns() { return 3; }
public function getRows() { return 10; }
public function getColumns()
{
return 3;
}
public function getRows()
{
return 10;
}
public function getLabelColumnSpacing() { return $this->columnSpacing; }
public function getLabelRowSpacing() { return $this->rowSpacing; }
public function getLabelColumnSpacing()
{
return $this->columnSpacing;
}
public function getLabelRowSpacing()
{
return $this->rowSpacing;
}
public function getLabelWidth() { return $this->labelWidth; }
public function getLabelHeight() { return $this->labelHeight; }
public function getLabelWidth()
{
return $this->labelWidth;
}
public function getLabelHeight()
{
return $this->labelHeight;
}
public function getLabelBorder() { return 0; }
public function getLabelBorder()
{
return 0;
}
}
?>

Some files were not shown because too many files have changed in this diff Show More