Compare commits

...

884 Commits

Author SHA1 Message Date
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
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
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
snipe
bbf69bc582 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-08 21:26:15 +01:00
snipe
f2b7a3d002 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-08 21:25:31 +01:00
Godfrey M
3fd9e3ab56 include textareas input return 2025-07-08 12:02:56 -07:00
snipe
8e11466a54 Added query scope
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 22:09:23 +01:00
snipe
dade9797d5 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 21:46:13 +01:00
snipe
97c1e65ffc Fixed fieldname
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 20:58:26 +01:00
snipe
b4e22f4a21 Small fix for seat listing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 20:56:26 +01:00
snipe
58b6feb3ca Merge pull request #17356 from grokability/show-only-taken-licenses
[FD-49569 ] - Show only assigned in license tab
2025-07-07 17:05:38 +01:00
snipe
41c4920d45 Show only assigned in license tab
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 16:58:36 +01:00
snipe
d1ddd8de98 Re-add column selector
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 16:23:48 +01:00
snipe
f41307eb4a Merge pull request #17353 from grokability/fixes-#14295-send-acceptance-on-signing
Fixed #14295 - allow user to receive an email PDF upon signing
2025-07-07 15:59:03 +01:00
snipe
59de77feb0 Use company name if provided instead of site name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:55:40 +01:00
snipe
8ebbcf6e80 Removed console commands
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:55:31 +01:00
snipe
24c6e836dd Added checkbox toggle
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:51:24 +01:00
snipe
8e38b3898e Fixes #14295 - allow user to receive an email PDF upon signing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:38:36 +01:00
snipe
ce9a5e35c9 Merge pull request #17352 from grokability/fixed-#17273-notes-in-upcoming-audit
Fixed #17273 -  switch to HTML table from markdown
2025-07-07 14:42:43 +01:00
snipe
b092779697 Fixed #17273 - switch to HTML table from markdown
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 14:37:38 +01:00
snipe
ab30a96d16 Merge pull request #17327 from Godmartinz/asset_model_redirect
FIXED: #15861 adds a redirect option for asset model and previous page
2025-07-07 14:00:31 +01:00
snipe
dab0fb16ad Merge pull request #17291 from Godmartinz/fix_bulk_checkout_focus
Fixes #12094 - Adds focus to select2 in bulk checkout
2025-07-07 13:57:12 +01:00
snipe
5be398bc99 Merge pull request #17350 from grokability/tighter-control-on-company
Fixes #17302 - Tighter control on company
2025-07-07 13:45:08 +01:00
snipe
fe4172957f Merge pull request #17351 from grokability/smaller-pdfs
Fixed #17349 - enable_font_subsetting in PDFs
2025-07-07 13:33:29 +01:00
snipe
ff3a59d347 Fixed #17349 - enable_font_subsetting in PDFs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:32:25 +01:00
snipe
f9aedea26f Eager load admin
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:21:07 +01:00
snipe
5abd2c7151 Added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:12:16 +01:00
snipe
bfcaf4f37b Removed unecessary use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:36:59 +01:00
snipe
5f4e1835bc Removed unused method
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:36:16 +01:00
snipe
c1f1ae6b64 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:31:12 +01:00
snipe
c4fcc6c24e Removed direct scoping calls
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:26:30 +01:00
snipe
dd73ad9941 Merge pull request #17341 from Godmartinz/query_error_rb19824
Fixed #17193: perform Orderby before Collection in Bulk Assets Controller
2025-07-07 11:19:37 +01:00
snipe
ac21f7569f Merge pull request #17346 from grokability/add-video-uploads
[FD-49538] Add video/audio uploads
2025-07-07 11:15:19 +01:00
snipe
4ef0158da4 Use preview instead of image text
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:19:29 +01:00
snipe
4db3b3ba0e Use config array for extensions in restore tool
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:19:13 +01:00
snipe
dc43d85323 Check for audio files
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:15:30 +01:00
snipe
62651f381c Expand safe allowed inline files
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:13:57 +01:00
snipe
3e9098907a Use config file for file types/mimes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:11:31 +01:00
snipe
e18df250f8 Removed console debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:08:08 +01:00
Godfrey M
be5c5a51da get desired behavior of select-2 2025-07-02 11:20:04 -07:00
Godfrey M
a728fad675 perform orderBy on query before converting to a Collection 2025-07-02 11:06:35 -07:00
snipe
185629b310 Merge pull request #17338 from grokability/small-depreciation-tweaks
Fixed #1909 - Small depreciation tweaks
2025-07-02 18:37:21 +01:00
snipe
30ebea4f2d Return int count
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 18:24:27 +01:00
snipe
b135c1eac2 Updated language strings in view
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 18:15:06 +01:00
snipe
88fef73d6f Use new translation
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:54:10 +01:00
snipe
556a9039e9 New strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:55 +01:00
snipe
cdfe6c21c1 Tightened HTML, added translations
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:42 +01:00
snipe
b094ebdd66 Removed validation of > 0
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:32 +01:00
snipe
526a7ddea6 Merge pull request #17337 from grokability/fixes-#17112-ldap-location-set-to-0
Fixed #17112 - Set location ID to null instead of 0
2025-07-02 17:20:34 +01:00
snipe
bb5ad31cba Merge pull request #17336 from uberbrady/safer_deserialize
Use safer deserialization defaults
2025-07-02 17:15:48 +01:00
snipe
549da2efed Set location ID to null instead of 0
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:15:18 +01:00
snipe
e5e586dc43 Attempt to generalize companyable in company scope
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:12:55 +01:00
Brady Wetherington
8a682beb0e Use safer deserialization defaults 2025-07-02 14:49:12 +01:00
snipe
699e9f75c9 Fixed RB-19892
Check for location before trying to grab company property

Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 11:57:42 +01:00
snipe
759e30977b Merge pull request #17333 from grokability/fixes-#17326-dash-sorting
Fixed #17326 - sorting on dashboard
2025-07-02 11:43:48 +01:00
snipe
6cfdb49cc3 Fixed #17326 - sorting on dashboard
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 11:41:21 +01:00
snipe
1195121bf0 Merge pull request #17330 from uberbrady/add_escaping_to_action_logs
Add escaping to user_agent and remote_ip variables for API results
2025-07-01 23:30:41 +01:00
Brady Wetherington
8bc067b18b Add escaping to user_agent and remote_ip variables for API results 2025-07-01 23:22:09 +01:00
Godfrey M
76f59f7b85 fix variables 2025-07-01 12:52:16 -07:00
Godfrey M
55ebb4671f update check in and check out controllers 2025-07-01 12:44:26 -07:00
Godfrey M
8a9cf07063 editing controllers and edit blades for other categories 2025-07-01 12:31:55 -07:00
Godfrey M
ca9ff8cf19 set return type for RedirectOptions 2025-07-01 12:04:02 -07:00
Godfrey M
7217d9c427 adds redirect to previous page, use match instead of switch cases 2025-07-01 11:55:46 -07:00
Godfrey M
9d712ad8f1 clean up code 2025-07-01 10:56:02 -07:00
Godfrey M
f3e49e7010 add asset model as a redirect option 2025-07-01 10:43:00 -07:00
snipe
ba94f1b920 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:45:43 +01:00
snipe
edcd46dd67 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:44:46 +01:00
snipe
5cf6c89dde Merge pull request #17322 from grokability/fixes-#8484-add-supplier-to-license-relationship
Fixed #8484 - added supplier to license relationship
2025-06-30 11:29:07 +01:00
snipe
58676b1f83 Fixed #8484 - added supplier to license relationship
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:27:30 +01:00
snipe
8ff7c30e5a Merge pull request #17315 from grokability/traitify-uploads
Added HasUploads trait and remove uploads method for models
2025-06-27 20:00:28 +01:00
snipe
cd989768d4 Added HasUploads trait and remove uploads method for models
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 19:32:22 +01:00
snipe
6cbdefe3d9 Small regressions
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 13:04:02 +01:00
snipe
c6ecc0d8e8 Merge pull request #17311 from grokability/rwork-bulk-api-to-smaller-pr
Fixed #9413 and rework upload API for bulk and better responses (refactor of #16964)
2025-06-27 12:43:34 +01:00
snipe
e0f5663bf4 Requested changes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:37:11 +01:00
snipe
aafc8996c1 phpcbf fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:18:54 +01:00
snipe
128da40cbf Added comments
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:03:12 +01:00
snipe
ea0460e97e Remove unused API files controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 11:51:36 +01:00
snipe
d8e7123576 Added uploaded files API controllers and presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 11:37:31 +01:00
snipe
6f45ec655f Merge pull request #16857 from realchrisolin/generic_tape
Generic tape
2025-06-25 14:59:57 +01:00
snipe
ec17c168ea Merge pull request #17222 from grokability/fixes-#17221-move-table-featueres-into-js
Fixed #17221 - Moved common table elements to partial
2025-06-25 14:52:17 +01:00
snipe
119b097521 More cleanup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:43:09 +01:00
snipe
c731633a84 Fixed weird formatting
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:40:26 +01:00
snipe
6a4d6ade39 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:39:46 +01:00
snipe
b3b4697fc9 More whitespace cleanup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:38:25 +01:00
snipe
72c706d697 Aaaand one more
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:30:34 +01:00
snipe
8a7af24bd4 More whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:29:33 +01:00
snipe
dd01bd3e5f Cleaned up whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:29:01 +01:00
snipe
59cade9f82 Cleaned up whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:28:20 +01:00
snipe
6bb9b79832 Tweaked dashboard
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:29 +01:00
snipe
f8fe7b5803 Removed whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:19 +01:00
snipe
4d6279d61c Added JS to handle data-dash attribute overrides
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:04 +01:00
snipe
5ef581f328 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:10:51 +01:00
snipe
20a59c343e Merge pull request #17296 from uberbrady/improve_fmcs_locations_test
Fixed #17190 and [FD-49375]: Do FMCS testing 'async' to keep from blowing out the whole settings page
2025-06-25 13:10:54 +01:00
snipe
d6feb522b7 Merge pull request #17297 from grokability/fixes-#17259-#15239-external-avatar
Fixed #15239 and #17259 - better handle external avatars
2025-06-25 13:09:33 +01:00
snipe
951aee8292 Fixes #15239 and #17259 - better handle external avatars
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 13:03:51 +01:00
Brady Wetherington
6e2d7912b5 Do the FMCS testing 'async' to keep from blowing out the whole page 2025-06-25 12:59:37 +01:00
snipe
b20925b550 Fixed #17282 - removed erroneous update gate for user-license endpoint
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 11:03:06 +01:00
Godfrey M
483f684b04 adds focus to select 2 bulk checkout 2025-06-24 09:58:38 -07:00
snipe
26774b4193 Merge pull request #17287 from uberbrady/limit_license_seat_increments
Fixed [FD-45786] - Limit changing of asset seat count to no more than 10k at a time
2025-06-24 17:43:02 +01:00
Brady Wetherington
a7a597d609 Added some tests around license seat changes 2025-06-24 17:35:49 +01:00
snipe
1c12b9278a Merge pull request #17116 from marcusmoore/17065-send-alert-to-assigner-upon-response
Fixed #17065 - allow sending acceptance alert to initiator.
2025-06-24 17:30:47 +01:00
Brady Wetherington
de4764bd05 Limit changing of asset seat count to no more than 10k at a time 2025-06-24 13:53:11 +01:00
snipe
c1e7a78d23 Merge pull request #17284 from Godmartinz/fix-depreciation-choice-in-transformer
FIXED: #14869 changes the depreciation method selected for Assets index table
2025-06-23 20:49:25 +01:00
Godfrey M
8894bb91cc fix method choice in asset transformer for depreciations 2025-06-23 12:37:29 -07:00
snipe
c955126f01 Merge pull request #17283 from Godmartinz/multiclick_checkout_bug
Fixed #14077: Disables checkout button after submitting
2025-06-23 20:30:38 +01:00
Godfrey M
ce53b48d04 disable checkout button after submitting 2025-06-23 12:18:24 -07:00
snipe
6015aeddee Merge pull request #17209 from Godmartinz/saml_xml_update_bug
Fixed #17089: SAML metadata now updating with new XML uploads
2025-06-23 16:40:35 +01:00
snipe
7b04e30964 Merge pull request #17280 from grokability/fixes/#7246-manager-id-in-import
Fixed #7246 - added manager employee/username number to importer
2025-06-23 16:07:14 +01:00
snipe
6794f5e783 Added logging for manager import
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:46:04 +01:00
snipe
6e41ceff39 Fixed parameter order
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:41:56 +01:00
snipe
7ab47ff0de Fixed #7246 - added manager employee number to importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:11:57 +01:00
snipe
92d24d8702 Merge pull request #17277 from grokability/#17264-add-notes-to-bulk
Fixed #17264: add notes to bulk asset edit
2025-06-23 12:36:50 +01:00
snipe
bcbfd46682 Update controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:33 +01:00
snipe
bfd96a695f Add notes field and nulling checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:28 +01:00
snipe
f27e8534dc Make sure $item exists
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:15 +01:00
snipe
040cd7ddbf Updated string to use shorter version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 20:56:37 +01:00
snipe
8d6b21a076 Merge pull request #17241 from spencerrlongg/bug/17239-possible-500-when-checking-in-license-assigned-to-a-soft-deleted-user
Fixed #17239 - Add `withTrashed` User Search On License Checkin
2025-06-22 20:14:03 +01:00
snipe
2d36b25017 Merge pull request #17258 from Robert-Azelis/patch-13
Company info - user print assets
2025-06-22 20:13:24 +01:00
snipe
1f9e4306ae Fixed #17261 - check for either old or new label engine QR
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 20:07:30 +01:00
snipe
5242e0b36e Merge pull request #17270 from grokability/fixes/#17249-order-by-location-in-maintenances
Fixed #17249 - sort by location in asset maintenances
2025-06-22 19:41:14 +01:00
snipe
e50505532e Fixes #17249 - sort by location in asset maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:38:39 +01:00
snipe
f05ef18d55 Merge pull request #17269 from grokability/fixes/#17256-user-eula-view
Fixed #17256: fixed permissions for non-super-admins to view their own EULAs
2025-06-22 19:29:41 +01:00
snipe
f6eccd7277 Added profile routes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:30 +01:00
snipe
4d1258c64b Fixed downloadformatter
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:17 +01:00
snipe
103cbfd038 Use profile eula API view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:05 +01:00
snipe
47069ad3f4 Added stored eula method on profile
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:45 +01:00
snipe
317f620992 Added profile controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:19 +01:00
snipe
8f43694582 Added eula download with user check in profile controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:06 +01:00
snipe
df30076ffd Update gate for user
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:23:50 +01:00
snipe
f81750617e Fixed mismatched html header tag
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 17:30:13 +01:00
Robert-Azelis
e4534c4319 Company info - user print assets 2025-06-20 10:33:09 +02:00
snipe
d6c09aae6b Updated paveit command to delete by user_id instead of ID
Signed-off-by: snipe <snipe@snipe.net>
2025-06-18 21:15:04 +01:00
spencerrlongg
d3d5230d0c add with trashed to user search 2025-06-17 19:15:13 -05:00
Marcus Moore
6f3c5c44a5 Remove assertion 2025-06-17 14:31:14 -07:00
Marcus Moore
67b32ca14d Mail content improvements 2025-06-17 14:22:54 -07:00
Marcus Moore
bffaf477ea Method order 2025-06-17 14:22:44 -07:00
Marcus Moore
cba45ece12 Add image 2025-06-17 13:57:40 -07:00
Marcus Moore
3290d7f401 Add translations 2025-06-17 13:27:05 -07:00
Marcus Moore
ef3827376d Add todo 2025-06-17 13:17:42 -07:00
Marcus Moore
4ae8a91051 Remove unused method 2025-06-17 12:34:43 -07:00
Marcus Moore
ff4819ac68 Get licenses working 2025-06-17 12:34:16 -07:00
Marcus Moore
58af133853 Re-add some test cases 2025-06-17 11:51:33 -07:00
Godfrey M
8199cd2118 comment merge methods for now 2025-06-17 10:59:57 -07:00
snipe
9f02b80cf1 Merge pull request #17233 from grokability/fixes-#17232-duplicate-expected-checkin-field
Fixed #17232 - removed duplicate expected_checkin field on asset edit/create
2025-06-17 18:14:36 +01:00
snipe
d3e4e81168 Fixed #17232 - removed duplicate expected_checkin field
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 18:08:21 +01:00
snipe
38195c0a8f Merge pull request #17225 from grokability/fixes-ui-issues-in-manager-view
Fixes #17227 and #17228 - UI issues in manager view
2025-06-17 17:28:52 +01:00
snipe
5fa11e4278 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:57:26 +01:00
snipe
c39b52fcb5 Move select list closer to the table, removed awkward icon
This makes the view more consistent with the normal layout and removes the CSRF that was being passed in the GET on the form.submit

Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:57:17 +01:00
snipe
ec65fc1e65 Move manager view option to misc section
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:56:56 +01:00
snipe
32d8646d96 Add @lukaskraic as a contributor 2025-06-17 16:26:17 +01:00
snipe
e8835fc2b1 Merge pull request #17096 from lukaskraic/feature/manager-view-v2
Manager View Feature
2025-06-17 16:24:40 +01:00
snipe
054a06c5dc Update resources/lang/en-US/admin/settings/general.php
Co-authored-by: Marcus Moore <contact@marcusmoore.io>
2025-06-17 15:39:26 +01:00
snipe
9c61d2eb22 Removed common elements in tables
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 14:25:02 +01:00
snipe
d66b6cfee6 Merge pull request #17220 from grokability/added-advanced-search-to-activity-log
Added advanced search to activity log
2025-06-17 13:24:31 +01:00
snipe
89c0427b2f Added fullscreen option
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:24:02 +01:00
snipe
3fec10d447 Added advanced search to activity log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:20:39 +01:00
snipe
f8b4981bfe Merge pull request #17219 from grokability/fixes-#17213-search-by-admin
Fixes #17213:  search by admin (created_by) in activity report
2025-06-17 13:02:51 +01:00
snipe
130669a2f9 Added adminuser relationship in Searchable to concat first name and last name
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:01:57 +01:00
snipe
c2c79ee231 Make created_by searchable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:56:39 +01:00
snipe
86f10bd702 Added employee number to searchable relation
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:56:30 +01:00
snipe
b496a06fc0 Merge pull request #17218 from grokability/fixed-#17212-hide-columns-on-print
Fixed #17212 - hide columns from print view
2025-06-17 12:37:43 +01:00
snipe
f865a6cb37 Added 'printIgnore' => true, to presenters to hide checkbox and action column from print view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:35:05 +01:00
snipe
89186ea4f8 Merge pull request #17195 from grokability/#17192-fixes-wonky-layout-on-bulk-edit
Fixed #17192 - wonky layout on bulk edit screens
2025-06-17 11:55:31 +01:00
snipe
fb19985186 Use the same variable for fieldset closing tag
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 11:52:47 +01:00
snipe
ebc6e1221a Merge pull request #17198 from akemidx/ghissue-16241
Fixed #16241 - menu items not getting class="active" on click.
2025-06-17 11:40:56 +01:00
snipe
2b91dcb700 Merge pull request #17216 from grokability/#17215-wrong-logo-check-in-print
Fixed #17215: Check for acceptance logo vs regular logo
2025-06-17 11:38:19 +01:00
snipe
9d2e333fd6 Check for acceptance logo vs regular logo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 11:36:10 +01:00
Godfrey M
013ad1069c saving certs sooner in the stack 2025-06-16 16:30:00 -07:00
Godfrey M
ec059717f6 adds id to XML textbox, now updates 2025-06-16 16:01:11 -07:00
akemidx
418566db3f fixes the easy ones 2025-06-16 18:41:36 -04:00
snipe
7be9463be6 Merge pull request #17180 from grokability/#12653-added-jobtitle-to-asset-listing
Fixed #12653 - added jobtitle to asset listing
2025-06-16 20:29:01 +01:00
snipe
51712bc7d6 Check for whether any of the models have a fieldset
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 16:40:07 +01:00
snipe
7b889d22d2 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 16:14:01 +01:00
snipe
e8aad989ec Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 14:27:47 +01:00
snipe
4006d64d60 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 14:25:59 +01:00
snipe
d792d99375 Removed stray space for favicon
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 03:51:50 +01:00
snipe
b11036a2e5 Fixed #11977 - changed path in backups language
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 03:40:54 +01:00
snipe
01de69a250 Merge pull request #17181 from grokability/fixed-#14542-missing-fullscreen-in-locations
Fixed #14542 - added fullscreen option for location view tabs
2025-06-15 02:57:51 +01:00
snipe
5e1c2e7feb Fixed #14542 - added fullscreen option for location view tabs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:56:07 +01:00
snipe
b842aa11e5 Remove debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:25:25 +01:00
snipe
ff01078b60 Fixed #12653 - adds job title to asset listing
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:23:53 +01:00
snipe
b1e92293fc Merge pull request #14998 from timoschwarzer/feature/department-manager-in-table
Added #14997: Display department manager in user view and list
2025-06-14 23:51:41 +01:00
snipe
443f69bd82 Merge branch 'develop' into feature/department-manager-in-table 2025-06-14 23:48:52 +01:00
snipe
f4decbf52e Fixed #15480 - adds location to acceptance email
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 23:08:37 +01:00
snipe
bd2c311e4f One more fix to bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 22:22:42 +01:00
snipe
2dcab6d0b3 Fixed logic on bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 22:21:00 +01:00
snipe
c68a97198f Fixes #8212 - force date format for purchase date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 20:40:15 +01:00
snipe
2702c3da2b Merge pull request #17178 from grokability/fixes-maintenance-name-on-delete
Fixed #17177 - use maintenance title for delete confirmation
2025-06-14 17:44:21 +01:00
snipe
da06e9afd5 Fixes #17177 - undefined title on asset maintenance
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 17:40:14 +01:00
snipe
1623f13539 Merge pull request #17147 from chrisnovakovic/php_upload_limit-hardening
Docker: harden updating of `php.ini` in entrypoint
2025-06-14 17:11:15 +01:00
snipe
5910982a4f Merge pull request #17176 from grokability/nicer-bulk-options-for-assets
Add logic around menu options
2025-06-14 17:06:49 +01:00
snipe
74630b36b0 Merge pull request #17175 from grokability/fixes/17172-checkin-date
Fixed #17172 - Better handle checkin date overrides
2025-06-14 17:06:30 +01:00
snipe
ace4a5d614 Add logic around menu options
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 17:02:09 +01:00
snipe
0d41947f64 Better handle checkin date overrides
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:54:25 +01:00
snipe
78de3b3591 Merge pull request #17174 from grokability/fixes-17163-permissions-on-maintenances
Fixed #17163 - insufficient permissions on editing an asset maintenance
2025-06-14 16:43:27 +01:00
snipe
0a4a6e7ba3 Removed unusued relationship
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:37:44 +01:00
snipe
090399b336 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:34:12 +01:00
snipe
47afb15970 Updated HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:24:05 +01:00
snipe
b1ba3376aa Validation for completion date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:13:57 +01:00
snipe
8c1e19e77c Fixed breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 15:53:05 +01:00
snipe
0801d1473c Use forem group div
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:57:55 +01:00
snipe
6d98878c72 Removed redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:31:15 +01:00
snipe
2d404fdadc Fixed breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:52 +01:00
snipe
b264fde165 Show title if it’s an edit
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:43 +01:00
snipe
970ff25e5e Added model and company to manufacturer view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:27 +01:00
snipe
9dd3eee65c Pull incorrect check for company scoping
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:04:09 +01:00
snipe
957faa6651 Fixed datepicker prepopulating with start_date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:03:52 +01:00
snipe
cc7dcc6e81 Disallow multiple on editing a maintenance
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 13:39:18 +01:00
snipe
f666cba104 Fixed RB-3991 - override route for non-existent fieldset
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 12:17:31 +01:00
Marcus Moore
7f35498919 Remove some test cases 2025-06-12 15:56:24 -07:00
Marcus Moore
297205ff91 Tests 2025-06-12 15:55:25 -07:00
Marcus Moore
6f99381d13 Handle accessories 2025-06-12 15:49:38 -07:00
Marcus Moore
45a42b00ad Add test 2025-06-12 15:48:48 -07:00
Marcus Moore
6586858284 Scaffold some test cases 2025-06-12 15:31:32 -07:00
Marcus Moore
19cce15e54 Revert "Method signature change"
This reverts commit a60ffc0702.
2025-06-12 13:54:53 -07:00
Marcus Moore
62b8e4c46f Revert "Inline some values"
This reverts commit 74b7d27408.
2025-06-12 13:54:46 -07:00
Marcus Moore
74b7d27408 Inline some values 2025-06-12 13:49:19 -07:00
Marcus Moore
a60ffc0702 Method signature change 2025-06-12 13:46:05 -07:00
snipe
9b8524ba27 Merge pull request #17109 from akemidx/bug/sc-28718-2
FIXED: Adding Total to Consumable View Page
2025-06-12 21:44:08 +01:00
Marcus Moore
32526d77b8 Add todo 2025-06-12 13:35:22 -07:00
snipe
ba7db8f7b3 Merge pull request #17092 from Godmartinz/fix-bulk_checkout_undeployable
Adds `hasUndeplyableStatus` check to bulk checkout
2025-06-12 21:29:26 +01:00
snipe
04712ad252 Merge pull request #17151 from Godmartinz/bulk-checkout-add-pending-status
Fixed #17028: Allows bulk editing assets with a status type of pending
2025-06-12 21:27:44 +01:00
Marcus Moore
5711a9e148 Fix test 2025-06-12 13:02:26 -07:00
Marcus Moore
a0f40c2dfb Tests 2025-06-12 12:56:24 -07:00
Marcus Moore
34636016eb Improve test 2025-06-12 12:47:03 -07:00
Marcus Moore
95027e329c Formatting 2025-06-12 12:44:29 -07:00
Marcus Moore
c9778a73c7 Wire up category controller 2025-06-12 12:32:34 -07:00
Marcus Moore
628d2a0a0a Flesh out mail contents 2025-06-12 12:12:38 -07:00
Marcus Moore
cc0ff1ec1f Handle missing recipient 2025-06-12 11:53:30 -07:00
snipe
6543540509 Merge pull request #17166 from Godmartinz/item_name_fix_from_account_eula_table
FIXED: #17136 loads item relation properly in account EULA tab
2025-06-12 11:53:51 +01:00
Marcus Moore
cd53fc6318 Scaffold email contents 2025-06-11 16:56:40 -07:00
Godfrey M
4934b6c4da fix query for item relationship to load properly 2025-06-11 11:42:16 -07:00
snipe
59db38524b Merge pull request #17094 from spencerrlongg/snipe-it-17051
Fixes #17051 - Nulling Custom Fields In Bulk Asset Updates
2025-06-11 10:31:28 +01:00
snipe
3f5cfc3a4b Merge pull request #17142 from marcusmoore/fixes/present-on-null-in-expected-checkins-notification
Fixed Expected checkin notification erroring on unknown users
2025-06-11 10:27:49 +01:00
snipe
3443f02c0a Merge pull request #17141 from marcusmoore/fixes/undefined-manages_users_count-on-user
Fixed bad method calls in user index api call
2025-06-11 10:26:13 +01:00
snipe
9a012ca01e Merge pull request #17154 from Robert-Azelis/patch-10
Update user print assigned assets
2025-06-11 10:24:50 +01:00
snipe
509ef34cca Merge pull request #17155 from Robert-Azelis/patch-11
Update location print assigned assets
2025-06-11 10:24:29 +01:00
snipe
49c289a094 Merge pull request #17158 from grokability/changes-default-history-column-visibility
Changed default visibility on history views
2025-06-11 10:19:33 +01:00
snipe
10e5d88fb6 Changed default visibility on history views
Signed-off-by: snipe <snipe@snipe.net>
2025-06-11 10:16:26 +01:00
snipe
ae64fb3fdb Merge pull request #17157 from grokability/fixed-#17138-category-type-case-sensitive
Fixed #17138 - category type was case-sensitive
2025-06-11 10:12:05 +01:00
snipe
cac2fde504 Fixed #17138 - category type in category importer is case sensitive
Signed-off-by: snipe <snipe@snipe.net>
2025-06-11 10:08:59 +01:00
snipe
bb38a96fd1 Merge pull request #17152 from marcusmoore/fixes/handle-category-missing-upon-checkin
Handle potentially missing category upon checkin
2025-06-11 09:36:50 +01:00
Robert-Azelis
79dbcb10c9 Update location print assigned assets
Lets allow user to select print layout: portrait or landscape
Use dedicated logo for PDF documents: acceptance_pdf_logo
2025-06-11 09:22:13 +02:00
Robert-Azelis
7c80fdea58 Update user print assigned assets
Lets allow user to select print layout: portrait or landscape
Use dedicated logo for PDF documents: acceptance_pdf_logo
2025-06-11 09:19:51 +02:00
Godfrey M
5500a42744 change message from error to warning 2025-06-10 15:10:10 -07:00
Godfrey M
ae46264707 removed comment 2025-06-10 15:07:42 -07:00
Godfrey M
f2d8665e54 adds tests 2025-06-10 15:07:04 -07:00
Marcus Moore
db7110d6b2 Remove test 2025-06-10 10:43:23 -07:00
Marcus Moore
4e06b597fe Handle category missing in Consumable 2025-06-10 10:43:02 -07:00
Godfrey M
fe006d05d3 allows to change the status to pending in bulk edit while deployed 2025-06-10 10:41:26 -07:00
Marcus Moore
358b70e280 Handle category missing in Accessory 2025-06-10 10:36:59 -07:00
Marcus Moore
a060dde625 Add failing test 2025-06-10 10:34:25 -07:00
Godfrey M
3cfed72af4 removes undeployables from asset_id array 2025-06-10 10:11:00 -07:00
Chris Novakovic
4c59989236 Docker: harden updating of php.ini in entrypoint
The Docker entrypoint scripts set values for the `upload_max_filesize`
and `post_max_size` directives in `php.ini` based on the value of the
`PHP_UPLOAD_LIMIT` environment variable, subject to the following
restrictions:

* Exactly one file matches `/etc/php/*/apache2/php.ini` (on Ubuntu) or
  `/etc/php*/php.ini` (on Alpine) - if, for example, more than one PHP
  package is installed in the base image, `PHP_UPLOAD_LIMIT` will not be
  honoured.
* The `php.ini` file already sets a non-default value for the
  `upload_max_filesize` or `post_max_size` directives - this is
  currently the case for the configurations inherited from upstream, but
  is not guaranteed. If the default values are relied upon,
  `PHP_UPLOAD_LIMIT` will silently not be honoured (although the script
  output will claim that it is).

Iterate over the lines outputted by `file(1)` so `PHP_UPLOAD_LIMIT` is
honoured in all available `php.ini` files, and set `upload_max_filesize`
and `post_max_size` regardless of whether they already have a value set.
2025-06-10 13:39:46 +01:00
Marcus Moore
6c1adff5c8 Extract translation string 2025-06-09 15:33:39 -07:00
Marcus Moore
9293bdca06 Don't render link for Unknown User 2025-06-09 15:32:07 -07:00
Marcus Moore
beeccbfb44 Handle unknown users gracefully 2025-06-09 15:30:03 -07:00
Marcus Moore
0d3d2e2e78 Fix keyes 2025-06-09 12:55:38 -07:00
Marcus Moore
2af7605451 Add failing tests 2025-06-09 12:55:09 -07:00
snipe
976cc1c86f Fixed second incorrect string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-09 14:00:47 +01:00
snipe
cbbf3aa6c8 Fixed incorrect translation string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-09 13:40:34 +01:00
snipe
4f8ff98d5b Merge pull request #17129 from grokability/fixes#17127-added-note-to-eula-api
Fixed #17127 - added note to EULA info
2025-06-08 15:29:47 +01:00
snipe
d4fe81c290 Fixed #17127 - added note to EULA info
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:27:50 +01:00
snipe
cbdf03aa66 Nicer code formatting
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:18:59 +01:00
snipe
d756670c56 Check for supplier before trying to show
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:17:24 +01:00
snipe
9ef7b0e64a Fixed missing translation string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 20:41:36 +01:00
snipe
2d7c0f7e5f Merge pull request #16469 from grokability/fix_action_date
Fixed `action_date` in `action_logs`
2025-06-06 17:18:39 +01:00
snipe
ec8ddc197f Merge branch 'develop' into fix_action_date 2025-06-06 17:18:27 +01:00
snipe
b48e56bd46 Merge pull request #17110 from Godmartinz/allow-users-to-dl-eula-from-account
Fixed #17084 - Adds Eula table to User account area with download option
2025-06-06 17:07:19 +01:00
snipe
753ca93371 Merge pull request #17126 from grokability/fixes-#17023-multi-maintenances
Fixes #17023 - added ability to bulk add maintenances
2025-06-06 17:01:50 +01:00
snipe
04f71e7f6a Shorter syntax
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:58:10 +01:00
snipe
5d129dd420 Small form fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:56:45 +01:00
snipe
1c37c630aa Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:26:37 +01:00
snipe
d329d6104e Make supplier_id nullable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:55:54 +01:00
snipe
482723f3bc Updated blade to use multiple
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:47:07 +01:00
snipe
cbc025b1ff Updated save method to save multiple
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:46:55 +01:00
snipe
bf8ceceabe Add bulk maintenance handler
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:42:14 +01:00
snipe
fba4bba132 Make supplier not required anymore
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:41 +01:00
snipe
0ded40c037 Add multiselect to maintenance form
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:30 +01:00
snipe
6f486a37ff Added maintenance option to bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:02 +01:00
snipe
1ef822997b Merge pull request #17125 from grokability/fixed-17117-added-translation
Fixed #17117 - use translation for “site default”
2025-06-06 14:04:08 +01:00
snipe
ea66629e98 Fixed #17117 - use translation for “site default”
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 14:03:17 +01:00
snipe
84c9979fe3 Merge pull request #17124 from grokability/add-print-button-to-tables
Added print button to tables
2025-06-06 13:52:22 +01:00
snipe
867a992183 Added print button to tables
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 13:51:12 +01:00
snipe
c3ad7d649c Merge pull request #17122 from grokability/added_highlighting_to_search
Added highlighting on table search
2025-06-06 12:47:31 +01:00
snipe
fc250e228d Added highlighting on table search
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 12:43:47 +01:00
snipe
37e81568ea Merge pull request #17120 from grokability/fixes-11807-datepicker-blade-component
Fixed #11807 - Standardize date-picker
2025-06-06 11:44:54 +01:00
snipe
048d910d5b Use partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:33:17 +01:00
snipe
b162aba445 Added localization for datepicker
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:33:02 +01:00
snipe
974627849b Added datepicker partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:47 +01:00
snipe
e18e9f699e Use blade component in datepicker partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:37 +01:00
snipe
cc9209d2de Removed EOL date and purchase date partials
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:13 +01:00
snipe
dd3d264e63 Merge pull request #16675 from Godmartinz/null_location_id_fix
Adds a check for assigned target not being null when creating an asset
2025-06-06 08:30:31 +01:00
snipe
246f1373a8 Merge pull request #16948 from marcusmoore/bug/sc-29181
Require assigned_x to be integer on asset model
2025-06-06 07:57:20 +01:00
snipe
b993f4270e Merge pull request #17114 from grokability/fixes-16934-update-asset-by-id
Fixed #16934 and #17068 - update asset by ID in importer
2025-06-06 07:55:03 +01:00
snipe
6c51ef11b1 Merge pull request #17115 from marcusmoore/17067-limit-cc-if-acceptance-required
Fixed #17067: Allow only sending cc email when acceptance required
2025-06-06 07:41:26 +01:00
Marcus Moore
ae98f6276e Replace reject with declined 2025-06-05 17:24:57 -07:00
Marcus Moore
7424a5987b Add subject 2025-06-05 17:22:31 -07:00
Marcus Moore
2d08749207 Improve readability 2025-06-05 16:56:11 -07:00
Marcus Moore
db50e98ae3 Populate tests 2025-06-05 16:51:44 -07:00
Marcus Moore
7ec0925c69 Scaffold out tests 2025-06-05 16:44:44 -07:00
Marcus Moore
df1361aa43 Scaffold test 2025-06-05 16:42:31 -07:00
akemidx
6db04c86df making fields active. first pass 2025-06-05 18:46:33 -04:00
Marcus Moore
bec80b443c WIP: begin to send email 2025-06-05 15:15:22 -07:00
Marcus Moore
333501fe55 WIP: create mail class 2025-06-05 15:07:19 -07:00
Marcus Moore
063553d4f7 Scaffold scenarios 2025-06-05 14:57:15 -07:00
Marcus Moore
19b9e50281 Update livewire component for alert_on_response 2025-06-05 14:40:19 -07:00
Marcus Moore
2a68b4aeff Update test for updating category with alert_on_response 2025-06-05 14:36:06 -07:00
Marcus Moore
5e25150521 Add another test case 2025-06-05 14:26:56 -07:00
Marcus Moore
cb183d3645 Store alert_on_response_id on CheckoutAcceptance 2025-06-05 14:06:22 -07:00
Marcus Moore
96bce301a0 Add alert_on_response to Category 2025-06-05 13:43:20 -07:00
Marcus Moore
360f5b7538 Add alert_on_response_id to CheckoutAcceptance 2025-06-05 13:10:18 -07:00
Marcus Moore
77234f6580 Extract translation strings 2025-06-05 12:24:46 -07:00
Marcus Moore
088e6af0b5 Remove admin_cc_email validation for admin_cc_always 2025-06-05 12:07:14 -07:00
Lukas Kraic
16fb1018a2 List users code refactoring 2025-06-05 20:05:38 +02:00
Godfrey M
e2e54677ee add auth to api call, gave more specificity to the relationship 2025-06-05 10:59:16 -07:00
Lukas Kraic
ad6fe855a9 Fix code style: Change comments 2025-06-05 18:53:36 +02:00
Lukas Kraic
c50c97d149 Fix code style: Change comments 2025-06-05 18:18:07 +02:00
Lukas Kraic
8b98ae15f0 Fix code style: Add comment 2025-06-05 18:08:34 +02:00
Lukas Kraic
261f84d5f5 Fix code style: Remove comment 2025-06-05 17:55:46 +02:00
Lukas Kraic
29989ac24e Fix code style: Remove empty line after end comment 2025-06-05 13:50:57 +02:00
snipe
7a93e94fa6 Add ID to field list
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 12:35:30 +01:00
snipe
e33f73fe9f Fixed comment text
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 11:56:14 +01:00
snipe
6291389df5 Fixed #16934 - update asset by ID in importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 11:53:57 +01:00
snipe
ed817dc414 Merge branch 'develop' into snipe-it-17051 2025-06-05 11:37:17 +01:00
Lukas Kraic
7494fa6bc9 Fix code style: Remove unnecessary end comment from short conditional block 2025-06-05 12:03:42 +02:00
Lukas Kraic
62e50dbe52 Fix code style: Add semantically correct end comments 2025-06-05 11:56:13 +02:00
Lukas Kraic
30c090ba2d Fix code style: Add descriptive end comment for conditional block 2025-06-05 11:45:05 +02:00
Lukas Kraic
7ff82e6043 Delete comments 2025-06-05 11:07:42 +02:00
snipe
2950fb1041 Merge pull request #17108 from Godmartinz/clear-buttons-for-custom-fields
Adds a clear button to custom radio buttons in bulk edit
2025-06-05 10:03:27 +01:00
Lukas Kraic
61d3e2fb49 Fix code style: Add required whitespace in end block comments 2025-06-05 09:23:58 +02:00
Lukas Kraic
fb18c1a0be Fix code style: Remove whitespace in end block comments 2025-06-05 08:59:07 +02:00
Godfrey M
68c082e0dc fix doc blocks 2025-06-04 15:48:35 -07:00
Godfrey M
ee3deb9c63 made eula api route, formatted table, cleaned up code 2025-06-04 15:04:59 -07:00
Marcus Moore
c1505de8d6 Update wording 2025-06-04 13:02:04 -07:00
Marcus Moore
10be434c13 Populate tests 2025-06-04 12:58:49 -07:00
Marcus Moore
92e22eead5 Formatting 2025-06-04 12:51:59 -07:00
Marcus Moore
31db86abd3 Simplify test 2025-06-04 12:51:48 -07:00
Marcus Moore
8e70ff135a Scaffold tests 2025-06-04 12:44:55 -07:00
akemidx
5ec52f7471 beginning of everything. tried some stuff but yea. something is up 2025-06-04 15:10:16 -04:00
Marcus Moore
8bc57f98a5 Split test case 2025-06-04 11:51:29 -07:00
Godfrey M
6f4cee6334 adds Eula tab and count to user account 2025-06-04 11:38:50 -07:00
Godfrey M
6beccc5e60 adds clear ability to custom radio buttons 2025-06-04 10:24:45 -07:00
Godfrey M
fed8e10644 Merge remote-tracking branch 'upstream/develop' into clear-buttons-for-custom-fields 2025-06-04 10:06:59 -07:00
snipe
3c0121c1d0 Merge pull request #17107 from grokability/fixes-16218-add-centos
Fixed #16218 - added centos to the switch case
2025-06-04 15:33:23 +01:00
snipe
02021e3fb9 Fixed #16218 - added centos to the switch case
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 15:27:19 +01:00
Lukas Kraic
ea447365fa Fix Codacy warnings II 2025-06-04 15:40:17 +02:00
Lukas Kraic
60989d6766 Fix Codacy warnings 2025-06-04 15:17:55 +02:00
snipe
a2960dc653 Merge pull request #17103 from grokability/fixes-17102-add-cc-as-search-string-in-settings
Fixed #17102 - added keywords to admin settings search for notifications
2025-06-04 12:12:17 +01:00
snipe
702499dd79 Fixed #17102 - added keywords to admin settings search for notifications
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 12:09:57 +01:00
snipe
482c427e34 Merge pull request #17100 from uberbrady/add_deleted_at_index_to_action_logs
Fixed #16205 - Add an index to deleted_at for those with many deleted action_logs
2025-06-04 11:48:45 +01:00
snipe
67910490bd Merge pull request #17101 from grokability/fixes-16157-added-advanced-search-to
Fixed #16157 - Added advanced search to users
2025-06-04 11:48:24 +01:00
snipe
fdb5ab2293 Added advanced search to users
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 11:46:38 +01:00
Brady Wetherington
092d9d1e42 Add deleted_at index to action_logs for people with many deleted action_logs 2025-06-04 11:23:09 +01:00
snipe
e657f11531 Merge pull request #17099 from grokability/fixes-16240-localization-strings
Fixed #16240 - made additional strings translatable
2025-06-04 10:40:21 +01:00
snipe
9aac183318 Added aria tag for accessibility
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:40:05 +01:00
snipe
c8c2867305 Remove unused translation
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:39:08 +01:00
snipe
3e1f71026c Fixed #16240 - made additional strings translatable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:37:04 +01:00
snipe
dc562d8c20 Remove error log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 08:43:40 +01:00
Lukas Kraic
84ec5aea26 Manager View Feature 2025-06-04 09:07:26 +02:00
Marcus Moore
444c13c6ea Scaffold template 2025-06-03 17:10:15 -07:00
spencerrlongg
12d5e4f7d2 cleanup and test 2025-06-03 19:07:02 -05:00
spencerrlongg
0fb1639915 this works! 2025-06-03 18:06:41 -05:00
Marcus Moore
d01f7cf317 Adhere to admin_cc_always setting 2025-06-03 15:32:11 -07:00
spencerrlongg
03725c8e0c custom field null and filtering 2025-06-03 17:21:07 -05:00
Marcus Moore
3942489d21 Add Test suffix and scaffold test 2025-06-03 14:13:52 -07:00
Marcus Moore
51479c8bbc Scaffold failing test 2025-06-03 13:28:40 -07:00
Marcus Moore
ea3364ab68 Split test case 2025-06-03 13:19:44 -07:00
Godfrey M
cb608d7fd1 testing buttons out 2025-06-03 11:58:57 -07:00
Godfrey M
7129008428 remove testing changes 2025-06-03 11:44:38 -07:00
Godfrey M
3b832f507f fixes status check for bulk checkout 2025-06-03 11:38:59 -07:00
Godfrey M
b5849500f9 add isDeplyable check 2025-06-03 10:49:17 -07:00
snipe
9eaabf95a0 Make version number match tagged version :(
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 11:02:41 +01:00
snipe
724f38abc2 Merge pull request #16998 from kovacs-andras/develop
Bumped container image versions
2025-06-03 06:22:08 +01:00
snipe
0dfc083a91 Removed log error
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 06:07:06 +01:00
snipe
16432f503a Merge pull request #17087 from grokability/#17085-checkin-and-delete-not-nulling-assigned_type
Fixed #17085 - assigned_type not being nulled on asset delete+checkin
2025-06-03 05:53:24 +01:00
snipe
7c9433be5d Added migration to fix existing wonky data
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:37:37 +01:00
snipe
e4ce71ff14 Added time on action date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:53 +01:00
snipe
45c6406ff4 Added console command for fixup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:43 +01:00
snipe
550e2b6bb8 Null both assigned to and assigned type on delete
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:30 +01:00
snipe
a7bb890729 Removed action date from array of things to log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:29:49 +01:00
snipe
3d8f8faf01 Added action_date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:28:58 +01:00
snipe
55cf5877c4 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:28:45 +01:00
Marcus Moore
dec3c4aff3 Improve wording 2025-06-02 17:45:08 -07:00
Marcus Moore
79e00c1191 Require admin_cc_email if admin_cc_always is true 2025-06-02 17:21:58 -07:00
Marcus Moore
12dc33244d Start storing admin_cc_always 2025-06-02 17:20:01 -07:00
Marcus Moore
6e37f945ac Add test helpers 2025-06-02 17:13:37 -07:00
Marcus Moore
d75120000a Add failing tests 2025-06-02 17:11:18 -07:00
Marcus Moore
9e4aab7165 Scaffold tests 2025-06-02 17:05:18 -07:00
Marcus Moore
6bc3209333 Use @checked for inputs 2025-06-02 17:05:00 -07:00
Marcus Moore
054ff42547 Add migration for admin_cc_always 2025-06-02 17:03:14 -07:00
spencerrlongg
11b47b308b front end done, sloppy 2025-06-02 18:39:08 -05:00
Marcus Moore
367ab8ddd5 Add help text 2025-06-02 16:27:04 -07:00
Marcus Moore
4f5d4a0984 Scaffold settings page changes 2025-06-02 16:25:06 -07:00
snipe
40489c53d6 Merge pull request #17078 from grokability/fixes-#17076-validation-on-bulk-submit
Fixed #17076 - Disable optional status ID form field if value is blank/Do Not Change
2025-06-02 22:28:31 +01:00
snipe
93b760d53b Disable form fields if the value is blank
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 22:16:36 +01:00
snipe
e86996bc7e Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 18:15:21 +01:00
snipe
14244f45b6 Duplicates PR #16957
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 18:13:16 +01:00
snipe
1b9d90a322 Added over sixty test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 17:36:50 +01:00
snipe
736f74d083 Merge pull request #17074 from uberbrady/improve_api_rate_limiting
Fix to rate-limiter on higher rate-limits
2025-06-02 17:25:02 +01:00
Brady Wetherington
8194c6efdb Fix to rate-limiter on higher rate-limits 2025-06-02 17:21:14 +01:00
snipe
aae2a17ad1 Add @amedranogil as a contributor 2025-06-02 15:27:55 +01:00
snipe
f44150668c Merge pull request #17038 from amedranogil/develop
more robust php.ini update.
2025-06-02 15:27:28 +01:00
snipe
6eed2deb09 Merge pull request #17013 from Robert-Azelis/patch-9
API Models - added requestable for API request
2025-06-02 15:26:08 +01:00
snipe
878c6e7031 Merge pull request #17019 from grokability/#15320-status_to_bulk_checkout
Fixed #15320 - added status label to bulk checkout
2025-06-02 15:05:44 +01:00
snipe
1f4a73fab6 Merge pull request #17062 from grokability/add_category_importer
Added category importer
2025-06-02 15:03:31 +01:00
snipe
7a315523fe Improved CSV
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 15:01:16 +01:00
snipe
6f082e662b Fixed weird layout
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 14:52:41 +01:00
snipe
018c981c5a Merge pull request #17042 from marcusmoore/chore/replace-customfield-elements-macro-take-two
Replace customfield_elements form macro take two
2025-06-02 10:18:19 +01:00
snipe
0149773a03 Fixed variable name
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 04:14:51 +01:00
snipe
5d46d90725 Added category importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 04:11:54 +01:00
snipe
0544e05f32 Merge pull request #17061 from grokability/add_manufacturer_importer
Added manufacturer importer
2025-06-02 03:05:25 +01:00
snipe
80ff42a41f Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 03:01:56 +01:00
snipe
90b7df45b9 Added tests and support helper
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:55:11 +01:00
snipe
32858b973a Added sample CSV
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:55:00 +01:00
snipe
40ba8d0de1 Fixed “send welcome email” detection
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:34 +01:00
snipe
8ddbb4e64f Added manufacturer factory
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:21 +01:00
snipe
cc40c48aac Added manufacturers import fields
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:03 +01:00
snipe
522ab9e0f5 Added manufacturer importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:46 +01:00
snipe
97187aa7eb Skip manufacturers on checkout import type
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:37 +01:00
snipe
d93a5aa623 Added redirect after import
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:09 +01:00
snipe
a5b88982bf Added manufacturer icon
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:52:55 +01:00
snipe
df71bdcada Merge pull request #17044 from marcusmoore/bug/sc-29302
Handle missing location when rendering labels
2025-06-02 02:21:19 +01:00
snipe
51bab2dd26 Merge pull request #17045 from grokability/docker-laravel-log-permissions
Docker: Ensure permissions on Laravel log file
2025-05-30 06:52:31 +01:00
Jeremy Price
ed8da6ad1b Docker: Ensure permissions on Laravel log file
FIXES: https://github.com/grokability/snipe-it/issues/12725

In some of our Docker startups, it was possible for the Laravel log file
to be created with root permissions, causing future errors when the
non-root webapp tries to write to it.

We'll now always chown (and create, if necessary) the log file to the proper
user after running any artisan commands (as root)

We _could_ run them as the proper user via su, but IMO not doing so keeps the
script easier to read, but I'm not married to the approach. I'd still
want to keep the chown command(s) in, because it will also fix the
permissions for anyone who already has this issue.
2025-05-29 17:45:14 -07:00
Marcus Moore
18d0a04efc Avoid dumping pdf contents to test results 2025-05-29 15:05:08 -07:00
Marcus Moore
bb68ed3ad9 Handle asset not having location 2025-05-29 14:49:53 -07:00
Marcus Moore
402ca07aa2 Add failing test 2025-05-29 14:20:15 -07:00
snipe
28dc358df1 Merge pull request #17041 from grokability/improve_locations_and_supplier_api
Small refinements for suppliers and locations API and list view
2025-05-29 21:30:54 +01:00
Marcus Moore
3cf1e9d55d Remove customfield_elements macro 2025-05-29 13:30:23 -07:00
Marcus Moore
82b001ab5f Extract translation strings 2025-05-29 13:29:13 -07:00
Marcus Moore
7b272226ce Inline customfield_elements select 2025-05-29 13:29:13 -07:00
snipe
78d26fb7f6 Removed stray character
Typing is hard :(

Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:23:50 +01:00
snipe
930842e685 Removed unused method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:18:19 +01:00
snipe
b938cb42d8 Merge pull request #17040 from marcusmoore/improve-acceptance-reminder-output
Avoid displaying empty table in `SendAcceptanceReminder` command
2025-05-29 21:14:09 +01:00
snipe
4c7b6d130f Added additional search and display fields for suppliers and locations
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:13:08 +01:00
Marcus Moore
af57ca4983 Avoid displaying empty table 2025-05-29 11:55:43 -07:00
snipe
40c31a1ad7 Eager load adminuser method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:13:16 +01:00
snipe
7ae4a4177f Added created_by to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:13:05 +01:00
snipe
6efd323fbf Added adminuser method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:48 +01:00
snipe
ed9dbcc777 Added created_by to location presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:33 +01:00
snipe
c2cf7de41b Use presenter for suppliers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:16 +01:00
Alejandro M. Medrano Gil
32bd14bd2d more robust php.ini update.
could solve #10830 when setting PHP_UPLOAD_LIMIT environment variable in docker command and/or docker-compose.
2025-05-29 17:46:23 +02:00
snipe
f9cbecdb17 Merge pull request #17037 from grokability/supplier_importer
Added #17036 - suppliers importer
2025-05-29 16:15:59 +01:00
snipe
7bb29a0277 Added sample import CSVs
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:08:17 +01:00
snipe
d5f7579e9f Added columns to suppliers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:08:01 +01:00
snipe
13fd43c45c Added tests and test support
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:51 +01:00
snipe
c08ce901cc Added strings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:19 +01:00
snipe
94bd11d3c9 Added locations and supplier import types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:11 +01:00
snipe
59c6e26b29 Fixed mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:06:51 +01:00
snipe
bf7cc404f8 Set correct redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 15:54:37 +01:00
snipe
12a2c71b90 Added icon
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 14:47:59 +01:00
snipe
6e2eeba0f6 Added supplier importer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 14:47:54 +01:00
snipe
99a739fae3 Merge pull request #17035 from grokability/settings_style_improvements
Fixed #17034 - larger header color box on small views
2025-05-29 13:30:31 +01:00
snipe
0185f61c11 Fixed #17034 - larger header color box on small views
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 13:23:07 +01:00
snipe
783f0c113d Merge pull request #17009 from marcusmoore/chore/replace-user-skin-macro
Replace `skin` and `user_skin` macros with blade component
2025-05-29 13:07:47 +01:00
snipe
75a839cc21 Merge pull request #17010 from marcusmoore/chore/replace-two-factor-select-macro
Replace `two_factor_options` macro
2025-05-29 13:07:23 +01:00
snipe
577b5586b4 Merge pull request #17025 from akemidx/clear_button_on_date_picker
FIXED: Clear Button actually Clearing Dates on Date Picker
2025-05-29 12:37:56 +01:00
snipe
1474a16148 Merge pull request #17024 from akemidx/created_at_date_picker
ADDED: Created At date picker on Custom Reports
2025-05-29 12:37:17 +01:00
snipe
9baa2000e1 Merge pull request #17026 from marcusmoore/bug/translate-email-format
Reference correct translation string
2025-05-29 12:36:08 +01:00
snipe
d0624dbefe Merge pull request #17027 from akemidx/bug/sc-29295
FIXED: Translation strings in Username/Email formats
2025-05-29 12:34:47 +01:00
snipe
ecb6e8d9a9 Fixed route for custom fields index
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 12:24:22 +01:00
Robert-Azelis
bbb299faf2 Update AssetModelsController.php 2025-05-29 08:42:29 +02:00
snipe
ba3b55cab0 Merge pull request #17022 from marcusmoore/bug/sc-29281
Avoid dividing by zero in DefaultLabel
2025-05-29 02:42:41 +01:00
akemidx
67acca7bc8 fixing two translation strings. 2025-05-28 20:21:05 -04:00
Marcus Moore
e4b33c3b56 Reference correct translation string 2025-05-28 16:36:39 -07:00
akemidx
ed43c73cec clearing the date pickers 2025-05-28 18:48:22 -04:00
akemidx
a6fa795b41 clearing the date pickers 2025-05-28 18:48:08 -04:00
akemidx
3a6bac2e63 date picker 2025-05-28 18:43:24 -04:00
akemidx
8e01c05e42 date picker 2025-05-28 18:43:14 -04:00
akemidx
fabf9281e9 date picker 2025-05-28 18:35:43 -04:00
Marcus Moore
6588d409b8 Add validation 2025-05-28 14:45:03 -07:00
snipe
92a3421a4e Merge pull request #17020 from akemidx/column_persist_on_assigned_assets
FIXED: Column persist on User's Self View of Assigned Assets
2025-05-28 22:44:36 +01:00
Marcus Moore
a42147ff34 Enforce min of .1 for label width and height 2025-05-28 14:05:29 -07:00
Marcus Moore
0df1bc6894 Scaffold test 2025-05-28 14:04:12 -07:00
akemidx
9317076c5e adding cookie for Assigned Assets 2025-05-28 16:39:37 -04:00
snipe
1b5525c51f Added status label to view blade, variable to controller method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 20:51:21 +01:00
snipe
6019c80c7b Added blade element
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 20:50:59 +01:00
Marcus Moore
aaa6cb24d4 Scaffold test 2025-05-28 11:28:23 -07:00
snipe
1ef5ad500a Merge pull request #17017 from grokability/localization/translations-2025-05-28
Updated translations
2025-05-28 18:34:15 +01:00
snipe
9468acedfa Updated languages
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 18:20:02 +01:00
snipe
6a951b6357 Merge pull request #17012 from Robert-Azelis/patch-8
API Locations - added company_id for API request
2025-05-28 15:23:08 +01:00
snipe
95f7742259 Removed extra a href
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 15:21:23 +01:00
snipe
9f795306e5 Merge pull request #17014 from grokability/fix_breadcrumb_crash
Manually add API headers
2025-05-28 15:19:05 +01:00
snipe
6feaff1e7b Removed blade::render
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 15:12:14 +01:00
Robert-Azelis
9a168354ae Update AssetModelsController.php - added requestable for API request 2025-05-28 15:50:19 +02:00
Robert-Azelis
309d242c4d Update LocationsController.php - added company_id for API request 2025-05-28 15:45:22 +02:00
snipe
5c174f829e Merge pull request #16986 from grokability/api_throttle_headers
Fixed  #16961 - Manually add API headers
2025-05-28 13:47:16 +01:00
Marcus Moore
3c428f2d7b Inline two_factor_options macro 2025-05-27 16:40:08 -07:00
Marcus Moore
6833716576 Remove skin macro 2025-05-27 16:22:43 -07:00
Marcus Moore
bd374d031a Improve component name 2025-05-27 16:22:28 -07:00
Marcus Moore
ef26f48f60 Adapt for regular "skin" macro 2025-05-27 16:21:34 -07:00
Marcus Moore
b07f8525db Fix swapped yellow/yellow-dark 2025-05-27 16:13:05 -07:00
Marcus Moore
9f062701fa Add semi-colon 2025-05-27 16:03:22 -07:00
Marcus Moore
2c452daddf Replace user_skin macro with blade component 2025-05-27 16:00:47 -07:00
snipe
53a82d3f4d Merge pull request #17007 from marcusmoore/bug/sc-29278
Ensure boolean returned from method with boolean return type
2025-05-27 22:00:04 +02:00
Marcus Moore
b5b8816279 Avoid returning null from method that should return a boolean 2025-05-27 11:37:48 -07:00
snipe
7bc4127e8c Removed dupe header
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 15:01:54 +01:00
snipe
06158cc413 Add timestamp header
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:58:57 +01:00
snipe
cb49e7c9a6 Updated comments
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:32:47 +01:00
snipe
1822027a8f Extend the built-in ThrottleRequests middleware from Laravel
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:04:24 +01:00
snipe
c8dabc25e3 Added comment
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:03:56 +01:00
snipe
f2b10eeee8 Re-do the initial change :(
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 13:00:32 +01:00
snipe
4b52e1471c Remove unused use statement after refactor
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 18:45:21 +01:00
snipe
f6bba03375 Fixed dupe semicolon
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:50:19 +01:00
snipe
b3813a7121 Refactorered limiting headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:48:50 +01:00
snipe
eb2a1396ca Merge pull request #16999 from grokability/api_audt_fix
Better messaging when an asset fails validation on quick scan
2025-05-26 14:17:14 +02:00
snipe
0fae18c4ba Better handle missing asset payload because of RMB
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:14:28 +01:00
Andras Kovacs
25ac83e944 Bumped container image versions 2025-05-26 13:07:51 +02:00
snipe
a82e65e190 Add @Tinyblargon as a contributor 2025-05-26 11:09:12 +01:00
snipe
187bb90de0 Merge pull request #16993 from Tinyblargon/fix-16992
fix: `PHP_UPLOAD_LIMIT` not set for PHP 8.3
2025-05-26 12:08:36 +02:00
snipe
293648582a Improvements to API headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 10:52:14 +01:00
Tinyblargon
51a306993c fix: PHP_UPLOAD_LIMIT not set for PHP 8.3 2025-05-25 16:23:17 +02:00
snipe
ec1851fa84 More small carbon fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 19:27:58 +01:00
snipe
bbe748dbd3 Removed noisy log
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 19:05:39 +01:00
snipe
406e8c5874 Added test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 18:25:36 +01:00
snipe
a4f71a9f0a Manually add API headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 18:25:29 +01:00
snipe
3748498523 Merge pull request #16985 from grokability/move_faker_take_2
Moved faker out of dev reqs for seeding
2025-05-23 18:34:45 +02:00
snipe
49d11103f7 Fixed test namespace
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 17:28:40 +01:00
snipe
dce9060820 Moved faker out of dev reqs for seeding
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 17:28:00 +01:00
snipe
de6206ce78 Merge pull request #16982 from grokability/fixes#16958-expiring-assets-unarchived
Fixed #16958 - exclude archived assets from expiring assets report
2025-05-23 15:51:27 +02:00
snipe
a181ba308a Fixed #16958 - exclude archived assets from expiring assets report
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 14:48:54 +01:00
snipe
5a9ac01cf8 Set audit warnings as int
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 14:30:56 +01:00
snipe
a5cd306b1d Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 13:12:30 +01:00
snipe
57d3abab5f Merge pull request #16979 from marcusmoore/bug/sc-29178
Improved creator on accessory show page
2025-05-23 14:05:28 +02:00
snipe
b6eb3185d5 Merge pull request #16968 from marcusmoore/bug/sc-29233
Fixed potential slack webhook setting inconsistencies
2025-05-23 14:05:01 +02:00
snipe
286f78778c Merge pull request #16980 from uberbrady/add_new_checkin_checkout_counters_tests_rebased
New tests for checkin/checkout counters
2025-05-23 13:55:38 +02:00
Brady Wetherington
4b95790e2f WIP: new tests for checkin/checkout counters
note that the test isnt going to work

More WIP for checkout counters

Got all tests passing
2025-05-23 12:37:29 +01:00
snipe
97883971f7 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 10:43:32 +01:00
snipe
b3c12d4ee6 Wider text fields
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 10:42:24 +01:00
Marcus Moore
bea426d8e6 Add test 2025-05-22 16:54:52 -07:00
Marcus Moore
be60370eae Add test 2025-05-22 16:33:45 -07:00
Marcus Moore
7cc216eb38 Extract variable 2025-05-22 15:05:24 -07:00
Marcus Moore
8c90efe745 Add authorization check 2025-05-22 15:02:14 -07:00
snipe
4b9f4423f6 Merge pull request #16932 from marcusmoore/fixes/webhook-checkin-checkout-fix
Improve notifications
2025-05-22 10:53:43 +01:00
Marcus Moore
932b589bd9 Do not show section if user doesn't exist 2025-05-21 17:28:41 -07:00
Marcus Moore
ce18b91b03 Add translation 2025-05-21 16:49:15 -07:00
Marcus Moore
5f883310b5 Improve display of user on accessory show page 2025-05-21 16:48:26 -07:00
Marcus Moore
69cc46c2b8 Improve method name 2025-05-20 16:41:52 -07:00
Marcus Moore
dc6951f341 Improve method name 2025-05-20 16:28:49 -07:00
Marcus Moore
f7ed336f99 Remove typehint 2025-05-20 16:26:05 -07:00
Marcus Moore
47f287c031 Fix assertion 2025-05-20 16:25:22 -07:00
Marcus Moore
5739393f8d Check to make sure settings exist before attempting to update them 2025-05-20 12:45:23 -07:00
Marcus Moore
13956254ce Add migration to fix webhook settings 2025-05-20 12:27:19 -07:00
Marcus Moore
d6b69c8cc2 Fix webhook selected value in mount method 2025-05-20 12:00:58 -07:00
Marcus Moore
7a79fd18f0 Bind the selected webhook and avoid clearing from database 2025-05-20 11:40:44 -07:00
snipe
e3ffe79c4c Merge pull request #16962 from Godmartinz/audit_notifications_fix
Added dynamic properties to audit notifications
2025-05-20 12:34:33 +02:00
Marcus Moore
043325b966 Merge branch 'develop' into fixes/webhook-checkin-checkout-fix
# Conflicts:
#	app/Listeners/CheckoutableListener.php
2025-05-19 14:41:05 -07:00
Marcus Moore
a2696b799f Standardize test names 2025-05-19 14:33:18 -07:00
snipe
eb8ef37808 Merge pull request #16963 from Godmartinz/fix-double-scroll-bar-permission-groups
Removes double scrollbar from groups
2025-05-19 20:32:30 +02:00
Godfrey M
8416c6df05 removes double scrollbar 2025-05-19 11:21:52 -07:00
Godfrey M
3aa4814342 allow dynamic assignment to audit notifications 2025-05-19 11:08:03 -07:00
snipe
186f322bb5 Merge pull request #16953 from uberbrady/fix_checkin_emails
Make checkin emails not send when not configured to be
2025-05-16 11:14:31 +02:00
Brady Wetherington
28e2e7c924 Get rid of more editorialization 2025-05-16 10:57:34 +02:00
Brady Wetherington
97351028b5 Get rid of editorializing in comments 2025-05-16 10:56:36 +02:00
Brady Wetherington
0ef0863b59 Make checkin emails not send unless the send-emails attribute is set on the category 2025-05-16 10:48:17 +02:00
Marcus Moore
2e80b4ace8 WIP 2025-05-15 17:50:07 -07:00
Marcus Moore
0d896c2ef6 Extract method 2025-05-15 17:44:27 -07:00
Marcus Moore
dc9df04237 WIP 2025-05-15 17:40:51 -07:00
Marcus Moore
b469a64db3 WIP 2025-05-15 17:35:28 -07:00
Marcus Moore
73f19ff4e7 Scaffold tests 2025-05-15 17:30:06 -07:00
Marcus Moore
22b4fac3ee Formatting 2025-05-15 17:24:56 -07:00
Marcus Moore
c97884c8b0 WIP 2025-05-15 16:59:10 -07:00
Marcus Moore
b972fb514a Remove type hint 2025-05-15 16:54:46 -07:00
Marcus Moore
a3871bd1f2 WIP 2025-05-15 16:47:29 -07:00
Marcus Moore
2069f99b2b WIP 2025-05-15 16:42:20 -07:00
Marcus Moore
882d55fd09 Improve tests 2025-05-15 16:31:07 -07:00
Marcus Moore
68ef975726 WIP 2025-05-15 16:22:31 -07:00
Marcus Moore
0685ff3818 WIP 2025-05-15 16:12:51 -07:00
Marcus Moore
b28839d907 Scaffold tests 2025-05-15 15:57:52 -07:00
Marcus Moore
7f0a947de4 WIP 2025-05-15 15:43:57 -07:00
Marcus Moore
c8fc4afe65 Fix method name 2025-05-15 15:31:18 -07:00
Marcus Moore
3b7162cb02 Improve test setup 2025-05-15 15:30:16 -07:00
Marcus Moore
4245456382 Scaffold more tests 2025-05-15 15:27:15 -07:00
Marcus Moore
cdf43e31e2 Remove randomness from factory 2025-05-15 15:14:01 -07:00
Marcus Moore
f19d6b3c52 Scaffold tests 2025-05-15 15:13:50 -07:00
snipe
ca66e29072 Merge pull request #16946 from marcusmoore/bug/sc-29166
Gracefully handle error when editing of soft deleted users
2025-05-15 22:46:48 +02:00
Marcus Moore
6ff76a12f8 Strengthen test scenario 2025-05-15 13:39:27 -07:00
Marcus Moore
8b13997597 Merge branch 'develop' into fixes/webhook-checkin-checkout-fix
# Conflicts:
#	app/Listeners/CheckoutableListener.php
2025-05-15 13:38:20 -07:00
Marcus Moore
9600adee6b Don't allow viewing edit page if user soft deleted 2025-05-15 11:14:15 -07:00
snipe
6eb3819492 Apply correct unique custom field validation for audit API
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 18:31:49 +02:00
snipe
95226f87bc Use same logic for auditStore for validating unique fields
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 18:02:23 +02:00
snipe
ee3ae803b9 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:51:28 +02:00
snipe
057667c425 Removed duplicate unique display in table
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:10:47 +02:00
Marcus Moore
02fa7daa1d Require assigned_x to be integer on asset model 2025-05-14 14:13:07 -07:00
Marcus Moore
2a2acf6d1c Add failing test 2025-05-14 14:09:48 -07:00
Marcus Moore
f644642fac Add comment 2025-05-14 12:48:32 -07:00
Marcus Moore
3374a9e5a9 Add assertion 2025-05-14 12:48:11 -07:00
Marcus Moore
d3a74a5740 Use route bound user instead of re-querying 2025-05-14 12:46:34 -07:00
Marcus Moore
c458cc904a Add failing test 2025-05-14 12:45:55 -07:00
snipe
5c7b74e17e Merge pull request #16943 from grokability/small-tweaks-to-status-label-text
Clearer text on status label types
2025-05-14 17:35:40 +02:00
snipe
e07b0f65a1 Clearer text on status label types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 17:33:12 +02:00
snipe
b06fd5bbca Merge pull request #16942 from uberbrady/quick_temp_fix_notifications
A quick check to make sure that webhooks still fire when email is off
2025-05-14 16:21:37 +02:00
Brady Wetherington
6306f78fe0 A quick check to make sure that webhooks still fire when email is off 2025-05-14 16:14:03 +02:00
snipe
da4bce0c89 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 15:02:43 +02:00
snipe
053e815206 Add @JassonCordones as a contributor 2025-05-14 15:02:43 +02:00
snipe
ab1053ecda Merge pull request #16916 from marcusmoore/bug/sc-29153
Handle potential hard exception in Asset@getImageUrl method
2025-05-14 14:52:33 +02:00
snipe
9a61a3391b Merge pull request #16921 from marcusmoore/chore/locale-select
Replace locales macro
2025-05-14 14:52:02 +02:00
snipe
d3c19e28ec Merge pull request #16918 from marcusmoore/bug/sc-29151
Handle displaying deleted creator of an accessory
2025-05-14 13:36:08 +02:00
snipe
87115f2e50 Merge branch 'develop' into bug/sc-29151 2025-05-14 13:35:38 +02:00
Marcus Moore
5f7aadfba0 Ensure CC emails are always sent for assets 2025-05-13 17:56:35 -07:00
Marcus Moore
e954e066b4 Scaffold tests 2025-05-13 17:41:19 -07:00
Marcus Moore
e336182d79 Formatting 2025-05-13 14:45:09 -07:00
Marcus Moore
3eb0743446 Separate checking for sending email and webhook notifications 2025-05-13 14:43:16 -07:00
Marcus Moore
95c1c37ab1 Invert method 2025-05-13 14:25:03 -07:00
Marcus Moore
a7054f0b1e Improve method name 2025-05-13 14:20:42 -07:00
Marcus Moore
e4bfabfabe Add failing tests 2025-05-13 14:12:51 -07:00
snipe
6b56929a06 Null operator for missing created_by record
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:44:25 +02:00
Marcus Moore
e3642bb513 Remove unneeded div 2025-05-12 17:07:17 -07:00
Marcus Moore
ca7c416e19 Remove the replaced locales form macro 2025-05-12 16:35:40 -07:00
snipe
ac6d964e28 Merge pull request #16920 from marcusmoore/fixes/remove-outline-from-label
Remove logo outline from L7162_B
2025-05-12 23:04:49 +01:00
Marcus Moore
e2772c816d Remove label logo outline from L7162_B label 2025-05-12 13:58:13 -07:00
Marcus Moore
916f7401f3 Handle creator being deleted 2025-05-12 12:30:32 -07:00
Marcus Moore
b53a71d523 Add failing test 2025-05-12 12:30:06 -07:00
Marcus Moore
510a2b0889 Formatting 2025-05-12 12:03:15 -07:00
Marcus Moore
73057454c6 Fix bug in getImageUrl method 2025-05-12 11:56:40 -07:00
Marcus Moore
5a6cf2a296 Add test for existing functionality 2025-05-12 11:44:25 -07:00
snipe
9e3e04521e Merge pull request #16900 from marcusmoore/fixes/user-full-name-accessor
Handle settings not being available in full name accessor
2025-05-10 12:26:19 +01:00
snipe
65dfbd02fe Use develop branch
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:55 +01:00
snipe
649ab53320 Updated codacy link
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:18 +01:00
snipe
9250624f79 Merge pull request #16909 from grokability/fixes-#16554-category-delete
Fixed #16554 - Added models to deletable check
2025-05-09 19:23:35 +01:00
snipe
995e2090f5 Added/updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:53 +01:00
snipe
9b91584776 Added models to deletable check
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:48 +01:00
snipe
8fd97ea501 Merge pull request #16908 from grokability/bug/sc-28724
Fixed #16535 - more info to side rail in accessories
2025-05-09 18:03:05 +01:00
snipe
a80b9ab362 Fixed #16535 - more info to side rail in accessories
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:48:02 +01:00
snipe
556e1081b3 Added two more selectors for byod
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:23:08 +01:00
snipe
b070916f0b Merge pull request #16907 from grokability/add_ids_to_menus
Fixed #16456 - added ids to sidenav options and bod
2025-05-09 17:22:19 +01:00
snipe
940caf14b0 Added ids to menu items
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:09:59 +01:00
snipe
76da1d6663 Added class to checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:04:19 +01:00
snipe
bafff9020a Merge pull request #16611 from Godmartinz/MS_teams_deprecation_update
Reworked MS Teams deprecation warnings and notifications visibility
2025-05-09 16:38:25 +01:00
snipe
0d5dca6456 Fixed #16690 - fallback to category image if no model image is present
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 14:46:27 +01:00
snipe
0f9b7119c0 Merge pull request #16905 from grokability/fixes_#16901
Fixed #16901 - use default currency for asset maintenance cost
2025-05-09 12:45:46 +01:00
snipe
d4181549e8 Fixes #16901 - use default currency for maintenance cost display
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 12:43:49 +01:00
Marcus Moore
d57f56e44f Handle settings not being available 2025-05-08 16:20:26 -07:00
Marcus Moore
f7777ca8a5 Replace Form::locales on user setup 2025-05-08 15:38:49 -07:00
Marcus Moore
fce5530bc7 Replace Form::locales on bulk edit users page 2025-05-08 15:31:41 -07:00
Marcus Moore
ad1530e9ff Replace Form::locales on user edit page 2025-05-08 15:17:15 -07:00
Marcus Moore
abb2dcbbe4 Replace Form::locales on localization page 2025-05-08 15:05:48 -07:00
Marcus Moore
437499c5df Fix input name 2025-05-08 15:04:45 -07:00
Marcus Moore
f739c2c84a Introduce locale select component and make replacement on profile page 2025-05-08 15:02:35 -07:00
snipe
7d9b87f059 Merge pull request #16898 from marcusmoore/chore/form-radio-replacement
Replaced Form::radio helpers
2025-05-08 20:50:29 +01:00
Marcus Moore
c157f4190e Replace Form::radio in location partial 2025-05-08 12:25:48 -07:00
Marcus Moore
9357eca1cd Replace Form::radio on asset checkin page 2025-05-08 12:16:55 -07:00
snipe
40c65a07a4 Merge pull request #16896 from grokability/removed_seat_number
Removed seat "name" from licenses seats API/UI response
2025-05-08 17:57:40 +01:00
snipe
13521bcf75 Removed seat “name” from license seats API/UI
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 17:37:27 +01:00
snipe
1c09dc139a Undo previous change
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:40:24 +01:00
snipe
d5f955b1e0 License seats are not numbered correctly [sc-29113]
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:25:49 +01:00
snipe
9e6e8f0931 Moved incomplete test marker
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 16:22:09 +01:00
snipe
c93ef30801 Ignore flaky test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:43:40 +01:00
snipe
3e0dec4856 Fixed #16893 - more specific upload failure text
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:38:38 +01:00
snipe
0b167f5f6f Grab location uploads from backup
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:22:26 +01:00
snipe
f6b21fdb82 Merge pull request #16895 from grokability/fixed_#16863_custom_fields_validation
Fixed #16863 - better handle custom fields validation when unique but not required
2025-05-08 15:09:04 +01:00
snipe
f151628808 Merge pull request #16894 from grokability/resolve-webserver-permissions
Fix webserver/user file permissions issue
2025-05-08 15:08:40 +01:00
snipe
e44aad0328 Fixed typeos 2025-05-08 15:08:14 +01:00
snipe
1881054c92 Fixed #16863 - better handle unique not required custom field redirects
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 15:00:43 +01:00
Jeremy Price
f7533c5e41 Fix webserver/user file permissions issue
Fixes https://github.com/grokability/snipe-it/issues/16777

We weren't adding the webserver user to the app-user's group, which was
a problem for the webserver trying to write to the log file if it had
been created by a user-owned process (like a cron) or the installation
script chown-ing everything... even though the log file was created 664

This would often present in mysterious ways. In the linked case, trying
to upload a cvs for import would fail with an unhelpful message, because
the actual error is swallowed in the generic error handler for the page.

I've filed an issue to hopefully help with that: https://github.com/grokability/snipe-it/issues/16893

Used this opportunity to condense some logic that was
identical between architectures,
2025-05-08 13:55:23 +02:00
snipe
f181e0fa55 Merge pull request #16877 from marcusmoore/bug/sc-29012
Allow updating asset model image via api
2025-05-08 06:27:49 +01:00
snipe
b04efdfefc Merge pull request #16889 from grokability/add_updated_range_to_custom_report
Added #16887 - last updated date range for custom report
2025-05-08 06:27:32 +01:00
snipe
352b935dee Merge pull request #16884 from marcusmoore/bug/sc-29097
Removed `2fa_authed` from session upon logout
2025-05-08 06:23:26 +01:00
snipe
0ba3b9975a Added #16887 - last updated date range for custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-05-08 06:21:06 +01:00
Marcus Moore
cc06187f31 Remove 2fa_authed from session upon logout 2025-05-07 14:04:33 -07:00
snipe
a916767392 Show the QR code on the asset page regardless of label settings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-07 11:34:21 +01:00
snipe
1c57bfaa39 Small cosmetic change to offset
Signed-off-by: snipe <snipe@snipe.net>
2025-05-07 11:28:01 +01:00
snipe
4a5adeb661 Fixed #16866 - use singular translation for custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-05-07 10:55:28 +01:00
snipe
01f9772291 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-07 10:37:10 +01:00
snipe
960b3aebed Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-05-07 10:24:07 +01:00
Marcus Moore
d75de73867 Allow updating asset model image via api 2025-05-06 17:13:23 -07:00
snipe
e75df97902 Merge pull request #16876 from grokability/switch-back-to-multiarch-docker-with-emulation
Move back to multiarch builds with emulation (for now)
2025-05-06 21:39:23 +01:00
Jeremy Price
5be14ec750 Move back to multiarch builds with emulation (for now)
Turns out it's not straightforward to have multiarchitecture images
within the same namespace, if you want to run each architecture's build
on native runners.

While we work on getting that going, we're moving back to
build-everything-on-intel-runners-with-emulation-for-arm

it means slowwwww arm builds, but it also means we should get our images
straightened out
2025-05-06 22:27:53 +02:00
snipe
717a82f46a Dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-05-06 21:27:02 +01:00
snipe
e40038900b Merge pull request #16875 from ubc-cpsc/bugfix/CVE-2025-46734
Fixes CVE-2025-46734: league/commonmark contains a XSS vulnerability in Attributes extension
2025-05-06 19:19:09 +01:00
Joël Pittet
099eabc240 Fixes CVE-2025-46734 2025-05-06 11:01:45 -07:00
snipe
3a4fa35398 Merge pull request #16874 from grokability/clone_breadcrumb_fix
Fixed breadcrumbs for cloning
2025-05-06 16:42:52 +01:00
snipe
500d6a0cc2 Merge pull request #16873 from grokability/redirect_on_audit
Redirect options on audit
2025-05-06 16:39:29 +01:00
snipe
38e5bf71bc Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-06 16:36:09 +01:00
Chris Olin
248a05a916 adds support for continuous 53mm and 0.59in printers 2025-05-06 11:26:52 -04:00
Chris Olin
2c141579dd adds support for GenericTape label printers, includes class for 53mm tape printer 2025-05-06 11:26:40 -04:00
snipe
45ff195f11 Fixed breadcrumbs for cloning
Signed-off-by: snipe <snipe@snipe.net>
2025-05-06 16:17:02 +01:00
snipe
ce543c8179 Use consistent icon
Signed-off-by: snipe <snipe@snipe.net>
2025-05-06 16:12:15 +01:00
snipe
5c11a8c1e0 Modified helper
Signed-off-by: snipe <snipe@snipe.net>
2025-05-06 16:12:06 +01:00
snipe
f013a4c5ea Merge pull request #16871 from grokability/one-more-docker-fix
One more docker repo name fix
2025-05-06 15:16:48 +01:00
Jeremy Price
194a22452e stop building intel image in arm server 2025-05-06 16:10:44 +02:00
Jeremy Price
930a6a2ac8 One more docker repo name fix
i feel like i'm taking crazy pills

also right-naming the arm builds again
2025-05-06 16:06:13 +02:00
snipe
444dea8b42 Merge pull request #16868 from grokability/fix-dockerhub-references
OMG fix 2 more (only 1 active) dockerhub repo references
2025-05-06 10:15:28 +01:00
Jeremy Price
6cc3f69c2a OMG fix 2 more (only 1 active) dockerhub repo references 2025-05-06 07:54:20 +02:00
snipe
36f3834ca5 Merge pull request #16865 from ntaylor-86/fixes/alerts-enabled-new-install
Fixed #16815: Avoids potential error when settings table is empty
2025-05-06 00:59:52 +01:00
Nathan Taylor
d4b73b4fb9 Avoids potential error when alerts table is empty
Updates the Kernel to use the null-safe operator
when accessing the alerts_enabled setting. This prevents
a potential error if the settings object is null.
2025-05-06 09:47:33 +10:00
snipe
b37f488117 Merge pull request #16864 from marcusmoore/update-contributors
Added missing contributors
2025-05-05 22:18:11 +01:00
snipe
ee0a9e834a Fixed BulkDeleteAssetsTest test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-05 22:12:08 +01:00
snipe
c776e0e7e9 Merge pull request #16851 from grokability/fix-dockerhub-repo
Fix dockerhub repo we're pushing to, and arm build names
2025-05-05 22:03:58 +01:00
Jeremy Price
54d3193b6f run arm builds on arm 2025-05-05 22:55:38 +02:00
Marcus Moore
326657c709 contributors:generate 2025-05-05 13:30:49 -07:00
Marcus Moore
c72e86ea2e Update casing for contributor 2025-05-05 13:29:05 -07:00
Marcus Moore
34b1ca29d3 Remove duplicate contributor (QveenSi) 2025-05-05 13:28:25 -07:00
Marcus Moore
1dc876a436 docs: add @austinsasko as a contributor 2025-05-05 13:19:37 -07:00
Marcus Moore
b28bc2c500 docs: add @drexljo as a contributor 2025-05-05 13:18:53 -07:00
Marcus Moore
c38d98b00a docs: add @jfwiebe as a contributor 2025-05-05 13:18:29 -07:00
Marcus Moore
7b83df088b docs: add @JemCdo as a contributor 2025-05-05 13:18:29 -07:00
Marcus Moore
c8b1240665 docs: add @jjasghar as a contributor 2025-05-05 13:18:28 -07:00
Marcus Moore
8a9d6bbdca docs: add @pottom as a contributor 2025-05-05 13:18:27 -07:00
Marcus Moore
6859b36e7c docs: add @dasjoe as a contributor 2025-05-05 13:18:26 -07:00
Marcus Moore
f0073c1528 docs: add @gl-pup as a contributor 2025-05-05 13:18:26 -07:00
Marcus Moore
f6b7e621b7 docs: add @gastamper as a contributor 2025-05-05 13:18:25 -07:00
Marcus Moore
108a0179ca docs: add @LeafedFox as a contributor 2025-05-05 13:18:24 -07:00
Marcus Moore
2baf65aa62 docs: add @m4us1ne as a contributor 2025-05-05 13:18:24 -07:00
Marcus Moore
74c4e9665e docs: add @lopezio as a contributor 2025-05-05 13:18:23 -07:00
Marcus Moore
ae9c22f327 docs: add @loganswartz as a contributor 2025-05-05 13:18:22 -07:00
Marcus Moore
37bca6febd docs: add @sniff122 as a contributor 2025-05-05 13:18:22 -07:00
Marcus Moore
30196793bd docs: add @KorvinSzanto as a contributor 2025-05-05 13:18:21 -07:00
Marcus Moore
3060282ffb docs: add @juhotaipale as a contributor 2025-05-05 13:18:20 -07:00
Marcus Moore
d63bba8db7 docs: add @juanfont as a contributor 2025-05-05 13:18:20 -07:00
Marcus Moore
2c12ee01a0 docs: add @CalvinSchwartz as a contributor 2025-05-05 13:18:19 -07:00
Marcus Moore
aa76424a74 docs: add @byronwolfman as a contributor 2025-05-05 13:18:18 -07:00
Marcus Moore
9188d6229e docs: add @benperiton as a contributor 2025-05-05 13:18:18 -07:00
Marcus Moore
5555f32ffe docs: add @arukompas as a contributor 2025-05-05 13:18:17 -07:00
Marcus Moore
59062980ff docs: add @hex128 as a contributor 2025-05-05 13:18:16 -07:00
Marcus Moore
f4aac5f0b7 docs: add @disc as a contributor 2025-05-05 13:18:16 -07:00
Marcus Moore
5c167aa2a9 docs: add @AlexanderWPapyrus as a contributor 2025-05-05 13:18:15 -07:00
Marcus Moore
8e1eed498e docs: add @MelonSmasher as a contributor 2025-05-05 13:18:14 -07:00
Marcus Moore
e449f39ea6 docs: add @fabiang as a contributor 2025-05-05 13:16:43 -07:00
Marcus Moore
97171e0e1c docs: add @Jarli01 as a contributor 2025-05-05 13:16:42 -07:00
Marcus Moore
5935ca4664 docs: add @dkmansion as a contributor 2025-05-05 13:16:42 -07:00
Marcus Moore
21c88cd311 docs: add @splashx as a contributor 2025-05-05 13:16:41 -07:00
Marcus Moore
5c786d8b70 docs: add @corydlamb as a contributor 2025-05-05 13:09:28 -07:00
Marcus Moore
d718d210ed docs: add @bricelabelle as a contributor 2025-05-05 13:09:27 -07:00
Marcus Moore
f50c5d22b8 docs: add @bmkalle as a contributor 2025-05-05 13:09:27 -07:00
Marcus Moore
c36f9a432e docs: add @terwey as a contributor 2025-05-05 13:09:26 -07:00
Marcus Moore
65b6b02b1d docs: add @xWyatt as a contributor 2025-05-05 12:54:08 -07:00
Marcus Moore
9c65d7c057 docs: add @Wouter0100 as a contributor 2025-05-05 12:54:08 -07:00
Marcus Moore
eab07834cf docs: add @valentyntu as a contributor 2025-05-05 12:54:07 -07:00
Marcus Moore
eb38f33baf docs: add @viclou as a contributor 2025-05-05 12:54:06 -07:00
Marcus Moore
462f9f2f39 docs: add @yannikp as a contributor 2025-05-05 12:54:05 -07:00
Marcus Moore
1021ccb230 docs: add @timwsuqld as a contributor 2025-05-05 12:54:05 -07:00
Marcus Moore
84e9a3a7d6 docs: add @p3nj as a contributor 2025-05-05 12:54:04 -07:00
Marcus Moore
c0060b3625 docs: add @sreyemnayr as a contributor 2025-05-05 12:54:03 -07:00
Marcus Moore
197aa12c61 docs: add @octobunny as a contributor 2025-05-05 12:54:02 -07:00
Marcus Moore
dfb2959751 docs: add @nixn as a contributor 2025-05-05 12:54:01 -07:00
Marcus Moore
95fb4f0e45 docs: add @rcmcdonald91 as a contributor 2025-05-05 12:54:00 -07:00
Marcus Moore
b2c729b7b8 docs: add @owalerys as a contributor 2025-05-05 12:53:59 -07:00
Marcus Moore
fe9b224a44 docs: add @nunomaduro as a contributor 2025-05-05 12:53:59 -07:00
Marcus Moore
fc4e8c68f2 docs: add @Scorcher as a contributor 2025-05-05 12:53:58 -07:00
Marcus Moore
6fb1c03908 docs: add @nticaric as a contributor 2025-05-05 12:53:57 -07:00
Marcus Moore
96ccfdb8cc docs: add @firefrei as a contributor 2025-05-05 12:53:57 -07:00
Marcus Moore
75fd07e057 docs: add @mzack5020 as a contributor 2025-05-05 12:53:56 -07:00
Marcus Moore
87fe69ecfb docs: add @Mateus-Romera as a contributor 2025-05-05 12:53:55 -07:00
Marcus Moore
19b47030ca docs: add @Nevets82 as a contributor 2025-05-05 12:53:54 -07:00
Marcus Moore
cf4e3fcc37 docs: add @snazy2000 as a contributor 2025-05-05 12:53:54 -07:00
Marcus Moore
bef4133f51 docs: add @smcpeck as a contributor 2025-05-05 12:53:53 -07:00
Marcus Moore
7ac24efced docs: add @cendai-mis as a contributor 2025-05-05 12:53:52 -07:00
Marcus Moore
d8d4a7075e docs: add @Shankschn as a contributor 2025-05-05 12:53:51 -07:00
Marcus Moore
dd4c9df6d1 docs: add @serkanerip as a contributor 2025-05-05 12:53:50 -07:00
Marcus Moore
da28c02b50 docs: add @SBrown2021 as a contributor 2025-05-05 12:53:50 -07:00
Marcus Moore
54858402e3 docs: add @McG800 as a contributor 2025-05-05 12:53:49 -07:00
Marcus Moore
b39d8cc0b9 docs: add @rosscdh as a contributor 2025-05-05 12:53:48 -07:00
Marcus Moore
4c7c33800a docs: add @rickheil as a contributor 2025-05-05 12:53:47 -07:00
Marcus Moore
d2c604a7ce docs: add @Nothing4You as a contributor 2025-05-05 12:53:47 -07:00
Marcus Moore
91243fb6c0 docs: add @mbrrg as a contributor 2025-05-05 12:53:46 -07:00
Marcus Moore
940a85888a docs: add @deloz as a contributor 2025-05-05 12:53:45 -07:00
Marcus Moore
280c12e22b docs: add @Galaxy102 as a contributor 2025-05-05 12:53:44 -07:00
Marcus Moore
39e644d048 docs: add @manu-crealytics as a contributor 2025-05-05 12:53:44 -07:00
Marcus Moore
9acb3e5935 docs: add @marcquark as a contributor 2025-05-05 12:53:43 -07:00
Marcus Moore
8ac5b5df61 docs: add @brandon-bailey as a contributor 2025-05-05 12:53:42 -07:00
Marcus Moore
434932599c docs: add @thinkl33t as a contributor 2025-05-05 12:53:42 -07:00
Marcus Moore
81b8c445c6 docs: add @vicleos as a contributor 2025-05-05 12:53:41 -07:00
Marcus Moore
002bb72a8d docs: add @herroworrd as a contributor 2025-05-05 12:53:40 -07:00
Marcus Moore
288770900e docs: add @robintemme as a contributor 2025-05-05 12:53:39 -07:00
Marcus Moore
f6f6a23f8b docs: add @wewhite as a contributor 2025-05-05 12:53:39 -07:00
Marcus Moore
522fa7be44 docs: add @Serdnad as a contributor 2025-05-05 12:53:38 -07:00
Marcus Moore
272d9e0552 docs: add @mink-adao-duy as a contributor 2025-05-05 12:53:37 -07:00
Marcus Moore
9060a3cc13 docs: add @ahpaleus as a contributor 2025-05-05 12:53:36 -07:00
Marcus Moore
b6a9c0e68b docs: add @DanielRuf as a contributor 2025-05-05 12:53:36 -07:00
Marcus Moore
b43ae5be13 docs: add @dkaatz as a contributor 2025-05-05 12:53:35 -07:00
Marcus Moore
6384041107 docs: add @seanborg-codethink as a contributor 2025-05-05 12:53:34 -07:00
Marcus Moore
89703cd9df docs: add @sorvani as a contributor 2025-05-05 12:53:33 -07:00
Marcus Moore
f6aa9f1318 docs: add @fe80 as a contributor 2025-05-05 12:53:33 -07:00
Marcus Moore
838e214b24 docs: add @phil-flip as a contributor 2025-05-05 12:53:32 -07:00
Marcus Moore
628c444cd4 docs: add @aranar-pro as a contributor 2025-05-05 12:53:31 -07:00
Marcus Moore
e8289b0f45 docs: add @JonathonReinhart as a contributor 2025-05-05 12:53:31 -07:00
Marcus Moore
86816f632f docs: add @test1337ahp as a contributor 2025-05-05 12:53:30 -07:00
Marcus Moore
494710306b docs: add @NebelKreis as a contributor 2025-05-05 12:53:29 -07:00
Marcus Moore
7a5fe4981f docs: add @mnemonicly as a contributor 2025-05-05 12:53:29 -07:00
snipe
a67b320cae Merge pull request #15907 from uberbrady/protect_assigned_to_assigned_type_rebased
Rebased version of #15629 - prevent setting assigned_to without setting assigned_type
2025-05-05 20:42:46 +01:00
Godfrey M
e3a2397b74 removed hiding the notifications icon 2025-05-05 10:28:08 -07:00
Godfrey M
3b34654dd7 Merge branch 'develop' into MS_teams_deprecation_update
# Conflicts:
#	resources/lang/en-US/admin/settings/message.php
2025-05-05 09:33:32 -07:00
Godfrey M
4b6437854c swapped out hard coded text with translation 2025-05-05 09:31:23 -07:00
snipe
9ef20997a5 Merge pull request #16861 from uberbrady/cherry_pick_avery_3490
adds support for Avery 3490 (Cherry-picked from 'master')
2025-05-05 15:01:00 +01:00
Calvin Schwartz
cfd10ae294 adds support for Avery 3490 2025-05-05 14:59:10 +01:00
snipe
9b85e9a071 Add @realchrisolin as a contributor 2025-05-05 14:44:04 +01:00
snipe
407962d998 Merge pull request #16790 from Godmartinz/empty_field_columns_labels
Adds Label fields offset as an option
2025-05-05 14:09:40 +01:00
snipe
1245289906 Add @chfsx as a contributor 2025-05-05 14:08:31 +01:00
snipe
76c19202ed Merge pull request #16806 from chfsx/patch-1
[FIX] set upload-limit
2025-05-05 14:07:42 +01:00
snipe
ada8195e2e Merge pull request #16860 from grokability/better_location_scope_check
Livewire component for smoother check for location companies
2025-05-05 14:04:23 +01:00
snipe
83d6e9ad8a Removed log error
Signed-off-by: snipe <snipe@snipe.net>
2025-05-05 14:00:05 +01:00
snipe
4469db0bd3 Livewire component for smoother check for location companies
Signed-off-by: snipe <snipe@snipe.net>
2025-05-05 13:55:28 +01:00
snipe
5e2dba5483 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-05-05 11:15:46 +01:00
Fabian Schmid
9de97694c3 [FIX] set upload-limit 2025-05-05 08:35:45 +02:00
Jeremy Price
8349065b0a stop buiulding intel on arm 2025-05-02 10:59:42 +02:00
Jeremy Price
bb82b2513e Fix dockerhub repo we're pushing to, and arm build names 2025-05-02 10:58:25 +02:00
snipe
e781c170f3 Merge pull request #16841 from Godmartinz/dark_mode_black_input_group_add_on_color_fix
remove duplicate input-group color corrections from theme skins
2025-05-01 01:48:49 +01:00
Godfrey M
43dfbd3d21 remove duplicate input-group color correction from other themes 2025-04-30 16:21:15 -07:00
Godfrey M
073c9f5f7c remove duplicate input-group color correction 2025-04-30 16:16:14 -07:00
snipe
f9d67dd431 Merge pull request #16840 from uberbrady/docker_backmerge
Copy changes from 'master' to develop for docker builds
2025-04-30 21:19:22 +01:00
Brady Wetherington
a2ea4c7fd0 Copy changes from 'master' to develop for docker builds 2025-04-30 21:12:10 +01:00
snipe
a0358e32d7 Removed escaping
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 16:46:06 +01:00
snipe
c2023c5c56 Ampersand showing in Custom fields [sc-29051]
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 16:25:44 +01:00
snipe
43c310c82d Merge pull request #16531 from akemidx/bug/sc-28715
FIXED: Purchase Cost Column Always Shown
2025-04-30 15:54:31 +01:00
snipe
939a0c44dc Merge pull request #16826 from Godmartinz/fix_multiple_inline_label_field_options
Reworked fix for for 24mm_D label indent errror
2025-04-30 15:47:57 +01:00
snipe
2b9cf1663b Merge pull request #16837 from grokability/fmcs_scoping_improvements
Small improvements to scoped views
2025-04-30 15:33:53 +01:00
snipe
0a29e90701 Smal improvements to scoping displays
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 15:24:42 +01:00
snipe
d1be13e7d4 Added button text to translations
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 14:32:29 +01:00
snipe
049a777ca8 Added buttons to dashboard
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 14:32:20 +01:00
snipe
0dcaa83a3e Nicer breadcrumb
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 14:22:13 +01:00
snipe
db706269e6 Fixed validation message
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 14:22:06 +01:00
snipe
4f72505dc3 Merge pull request #16836 from grokability/fixes/#16834_handle_bad_data_in_permissions
Fixed #16834 - better handle bad data in permissions
2025-04-30 13:56:59 +01:00
snipe
340f8b73a5 Updated comments
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 13:56:37 +01:00
snipe
6c6b37000a Better handle permissions with bad data
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 13:43:05 +01:00
snipe
b5c79624c6 Display notes in group listing
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 11:14:42 +01:00
snipe
49d66dedf4 Nicer existing image display
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 10:19:12 +01:00
snipe
ebbcdbc864 Nicer breadcrumb
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 10:11:55 +01:00
snipe
a18691c09f Set image path in singleton
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 10:11:48 +01:00
snipe
245b0b0f8f Added image path
Signed-off-by: snipe <snipe@snipe.net>
2025-04-30 10:11:40 +01:00
snipe
1d3b0478f9 Merge pull request #16829 from marcusmoore/fixes/contributors-fix
Fixed updating CONTRIBUTORS.md via cli
2025-04-30 01:23:59 +01:00
Marcus Moore
a5d0307532 Wrap username in quotes 2025-04-29 16:20:00 -07:00
snipe
7daecdd53f Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 23:17:03 +01:00
snipe
667b4a49c3 Merge pull request #16828 from grokability/chore/sc-29037
Add audit button to BS table partial, redirect if asset won't validate
2025-04-29 23:07:45 +01:00
snipe
2518e60a5e Corrected gates
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 22:59:05 +01:00
snipe
9ff8b62cee Updated icon
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 22:58:58 +01:00
snipe
5086c80658 Invoke a validator, redirect to edit screen if invalid
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 22:47:34 +01:00
snipe
cb852fc20f Added audit gate to API
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 22:47:12 +01:00
snipe
fb3b34e0f6 Add audit button to BS table partial
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 22:47:02 +01:00
snipe
8d4fc07f63 Updated dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 20:37:47 +01:00
snipe
a0514ad8c1 Merge pull request #16799 from marcusmoore/bug/sc-28990
Handle category being null in CheckoutableListener
2025-04-29 20:37:11 +01:00
snipe
4e03e525a4 Merge pull request #16797 from Godmartinz/dark_mode_green_fix
Fixed Dark Mode color choices for fieldset links
2025-04-29 20:35:59 +01:00
snipe
0efdebcfd8 Merge pull request #16827 from grokability/fixed_custom_field_fieldset_display
Fixed fieldset display if custom fields are not available
2025-04-29 20:26:14 +01:00
snipe
c7835d2d1d Fixed copypasta
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 20:08:34 +01:00
snipe
3e3bc0a347 Removed test tab
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 20:01:22 +01:00
snipe
184a22828f Use input vs get
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 19:22:10 +01:00
snipe
f26e27d23e Display in checkboxes not saving on custom [sc-29028]
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 19:21:50 +01:00
snipe
e717f1e780 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 19:21:18 +01:00
snipe
d1085a0f46 Move notes above custom fields
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 18:53:31 +01:00
Godfrey M
2e0913bb3b remove unused method 2025-04-29 10:49:19 -07:00
Godfrey M
851ae46ea9 reworked fix for for 24mm_D label indent errior 2025-04-29 10:45:29 -07:00
snipe
89a52b7551 Fixed fieldset display if custom fields are not available
Signed-off-by: snipe <snipe@snipe.net>
2025-04-29 18:09:32 +01:00
Marcus Moore
25fdde1807 Handle null category 2025-04-23 14:16:28 -07:00
Godfrey M
a26279e0b9 adds custom field link color correction to all dark modes 2025-04-23 12:38:58 -07:00
Godfrey M
5da492cbf5 set max to 5 2025-04-22 12:07:02 -07:00
Godfrey M
d871c529d1 fix input max, and help block position 2025-04-22 11:59:19 -07:00
Godfrey M
7c2c5ea98d adds Field offset option to labels 2025-04-22 10:50:20 -07:00
Godfrey M
c385b4a082 remove testing lines 2025-04-09 12:11:46 -07:00
Godfrey M
1b961346f0 added withInput to the redirects 2025-04-09 12:11:31 -07:00
Godfrey M
100db23210 add checks that the target is not null, and redirects back with error messages 2025-04-09 11:54:59 -07:00
Godfrey M
3e980a4c57 set location if target is set 2025-04-09 11:11:59 -07:00
Godfrey M
9a3ac41370 add default location as a fallback to asset validation 2025-04-09 11:02:45 -07:00
Godfrey M
4ef161214d notification icon only appears when there are notifications 2025-04-01 12:01:35 -07:00
Godfrey M
29d0380db3 reword warning messages, remove warning if webhook cleared and saved, deprecations only for superadmins 2025-04-01 11:53:32 -07:00
akemidx
e408b902f0 removing depreciation from purchase cost (unneeded, should go elsewhere if wanted) 2025-03-18 16:38:44 -04:00
snipe
0b48fd1465 Removed extra headers
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 13:05:31 +00:00
snipe
220537fbfb Updated presenter name
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 12:59:57 +00:00
snipe
df5437647b Add optional serial value in presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 12:43:38 +00:00
snipe
92b2da9b1b Added history tab to components
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:38 +00:00
snipe
ef56177372 Use presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:31 +00:00
snipe
cb7822576f Use new presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:19 +00:00
snipe
7ba361b10d Use date formatter for filestable
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:54 +00:00
snipe
55694fa2fc Added strings
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:40 +00:00
snipe
c825878c46 Added history presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:33 +00:00
snipe
80a69bfe90 Revert datetime to date
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 18:09:27 +00:00
snipe
d4dc8d2b79 Remove action_date from loggable as a changed field
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 17:43:07 +00:00
snipe
4e3df93349 Change action_date display to date from datetime
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 16:16:17 +00:00
snipe
38efc62900 Add index on action_date, copy from created_at
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 16:01:46 +00:00
Brady Wetherington
eccdcc373e parent 2220828b00
author Brady Wetherington <bwetherington@grokability.com> 1728320853 +0100
committer Brady Wetherington <bwetherington@grokability.com> 1733158021 +0000

Prevent setting assigned_to without setting assigned_type

Fixed tests to include assigned_type when setting assigned_to

Add new tests for assigned_to without assigned_type

Added tighter validation to assigned_to and assigned_type, new tests

Fixed wrong comment

Fixed tests to include assigned_type when setting assigned_to

Add new tests for assigned_to without assigned_type

Fixed wrong comment
2024-12-02 16:53:08 +00:00
Timo Schwarzer
a2ff8f9609 Add Department Manager to single and multiple user views 2024-07-15 12:27:29 +02:00
1737 changed files with 40828 additions and 7887 deletions

View File

@@ -2590,10 +2590,10 @@
]
},
{
"login": "QveenSi",
"login": "qveensi",
"name": "Yevhenii Huzii",
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
"profile": "https://github.com/QveenSi",
"profile": "https://github.com/qveensi",
"contributions": [
"code"
]
@@ -2607,15 +2607,6 @@
"code"
]
},
{
"login": "QveenSi",
"name": "Yevhenii Huzii",
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
"profile": "https://github.com/QveenSi",
"contributions": [
"code"
]
},
{
"login": "chrisweirich",
"name": "Christian Weirich",
@@ -3328,7 +3319,7 @@
},
{
"login": "36864",
"name": 36864,
"name": "36864",
"avatar_url": "https://avatars.githubusercontent.com/u/109086466?v=4",
"profile": "https://github.com/36864",
"contributions": [
@@ -3361,6 +3352,843 @@
"contributions": [
"code"
]
},
{
"login": "chfsx",
"name": "Fabian Schmid",
"avatar_url": "https://avatars.githubusercontent.com/u/6661332?v=4",
"profile": "http://sr.solutions",
"contributions": [
"code"
]
},
{
"login": "realchrisolin",
"name": "Chris Olin",
"avatar_url": "https://avatars.githubusercontent.com/u/1288116?v=4",
"profile": "https://www.chrisolin.com",
"contributions": [
"code"
]
},
{
"login": "mnemonicly",
"name": "Dan",
"avatar_url": "https://avatars.githubusercontent.com/u/3803132?v=4",
"profile": "https://github.com/mnemonicly",
"contributions": [
"code"
]
},
{
"login": "NebelKreis",
"name": "Nebel",
"avatar_url": "https://avatars.githubusercontent.com/u/43917728?v=4",
"profile": "https://github.com/NebelKreis",
"contributions": [
"code"
]
},
{
"login": "test1337ahp",
"name": "test1337ahp",
"avatar_url": "https://avatars.githubusercontent.com/u/132433803?v=4",
"profile": "https://github.com/test1337ahp",
"contributions": [
"code"
]
},
{
"login": "JonathonReinhart",
"name": "Jonathon Reinhart",
"avatar_url": "https://avatars.githubusercontent.com/u/1916566?v=4",
"profile": "https://github.com/JonathonReinhart",
"contributions": [
"code"
]
},
{
"login": "aranar-pro",
"name": "aranar-pro",
"avatar_url": "https://avatars.githubusercontent.com/u/484742?v=4",
"profile": "https://github.com/aranar-pro",
"contributions": [
"code"
]
},
{
"login": "phil-flip",
"name": "Phil",
"avatar_url": "https://avatars.githubusercontent.com/u/27019397?v=4",
"profile": "https://github.com/phil-flip",
"contributions": [
"code"
]
},
{
"login": "fe80",
"name": "Steffy Fort",
"avatar_url": "https://avatars.githubusercontent.com/u/6473460?v=4",
"profile": "https://fe80.fr/",
"contributions": [
"code"
]
},
{
"login": "sorvani",
"name": "Jared Busch",
"avatar_url": "https://avatars.githubusercontent.com/u/3302372?v=4",
"profile": "https://github.com/sorvani",
"contributions": [
"code"
]
},
{
"login": "seanborg-codethink",
"name": "seanborg-codethink",
"avatar_url": "https://avatars.githubusercontent.com/u/111956991?v=4",
"profile": "https://github.com/seanborg-codethink",
"contributions": [
"code"
]
},
{
"login": "dkaatz",
"name": "dkaatz",
"avatar_url": "https://avatars.githubusercontent.com/u/160669961?v=4",
"profile": "https://github.com/dkaatz",
"contributions": [
"code"
]
},
{
"login": "DanielRuf",
"name": "Daniel Ruf",
"avatar_url": "https://avatars.githubusercontent.com/u/827205?v=4",
"profile": "https://threema.id/74SF7MW6?text=",
"contributions": [
"code"
]
},
{
"login": "ahpaleus",
"name": "ahpaleus",
"avatar_url": "https://avatars.githubusercontent.com/u/38883201?v=4",
"profile": "https://github.com/ahpaleus",
"contributions": [
"code"
]
},
{
"login": "mink-adao-duy",
"name": "Anh DAO-DUY",
"avatar_url": "https://avatars.githubusercontent.com/u/22906055?v=4",
"profile": "https://github.com/mink-adao-duy",
"contributions": [
"code"
]
},
{
"login": "Serdnad",
"name": "Andres Gutierrez",
"avatar_url": "https://avatars.githubusercontent.com/u/4723453?v=4",
"profile": "https://github.com/Serdnad",
"contributions": [
"code"
]
},
{
"login": "wewhite",
"name": "Warren White",
"avatar_url": "https://avatars.githubusercontent.com/u/111083379?v=4",
"profile": "https://github.com/wewhite",
"contributions": [
"code"
]
},
{
"login": "robintemme",
"name": "Robin Temme",
"avatar_url": "https://avatars.githubusercontent.com/u/2809241?v=4",
"profile": "https://robintemme.de/",
"contributions": [
"code"
]
},
{
"login": "herroworrd",
"name": "herroworrd",
"avatar_url": "https://avatars.githubusercontent.com/u/47008367?v=4",
"profile": "https://github.com/herroworrd",
"contributions": [
"code"
]
},
{
"login": "vicleos",
"name": "vicleos",
"avatar_url": "https://avatars.githubusercontent.com/u/28558609?v=4",
"profile": "https://mubiu.com/",
"contributions": [
"code"
]
},
{
"login": "thinkl33t",
"name": "Bob Clough",
"avatar_url": "https://avatars.githubusercontent.com/u/1016780?v=4",
"profile": "http://thinkl33t.co.uk/",
"contributions": [
"code"
]
},
{
"login": "brandon-bailey",
"name": "Brandon Daniel Bailey",
"avatar_url": "https://avatars.githubusercontent.com/u/10648463?v=4",
"profile": "https://github.com/brandon-bailey",
"contributions": [
"code"
]
},
{
"login": "marcquark",
"name": "Marc Bartelt",
"avatar_url": "https://avatars.githubusercontent.com/u/23556080?v=4",
"profile": "https://github.com/marcquark",
"contributions": [
"code"
]
},
{
"login": "manu-crealytics",
"name": "manu-crealytics",
"avatar_url": "https://avatars.githubusercontent.com/u/18286893?v=4",
"profile": "https://github.com/manu-crealytics",
"contributions": [
"code"
]
},
{
"login": "Galaxy102",
"name": "Konstantin Köhring",
"avatar_url": "https://avatars.githubusercontent.com/u/18245993?v=4",
"profile": "https://www.galaxy102.de/",
"contributions": [
"code"
]
},
{
"login": "deloz",
"name": "Deloz",
"avatar_url": "https://avatars.githubusercontent.com/u/685167?v=4",
"profile": "https://deloz.net/",
"contributions": [
"code"
]
},
{
"login": "mbrrg",
"name": "Martin Berg",
"avatar_url": "https://avatars.githubusercontent.com/u/2682426?v=4",
"profile": "https://github.com/mbrrg",
"contributions": [
"code"
]
},
{
"login": "Nothing4You",
"name": "Richard Schwab",
"avatar_url": "https://avatars.githubusercontent.com/u/3694534?v=4",
"profile": "https://github.com/Nothing4You",
"contributions": [
"code"
]
},
{
"login": "rickheil",
"name": "Rick Heil",
"avatar_url": "https://avatars.githubusercontent.com/u/8959676?v=4",
"profile": "https://rickheil.com/",
"contributions": [
"code"
]
},
{
"login": "rosscdh",
"name": "Ross Crawford-d'Heureuse",
"avatar_url": "https://avatars.githubusercontent.com/u/397106?v=4",
"profile": "https://github.com/rosscdh",
"contributions": [
"code"
]
},
{
"login": "McG800",
"name": "Ryan McGuire",
"avatar_url": "https://avatars.githubusercontent.com/u/1621107?v=4",
"profile": "https://github.com/McG800",
"contributions": [
"code"
]
},
{
"login": "SBrown2021",
"name": "SBrown2021",
"avatar_url": "https://avatars.githubusercontent.com/u/77835667?v=4",
"profile": "https://github.com/SBrown2021",
"contributions": [
"code"
]
},
{
"login": "serkanerip",
"name": "Serkan",
"avatar_url": "https://avatars.githubusercontent.com/u/8780913?v=4",
"profile": "https://github.com/serkanerip",
"contributions": [
"code"
]
},
{
"login": "Shankschn",
"name": "Shanks",
"avatar_url": "https://avatars.githubusercontent.com/u/63188620?v=4",
"profile": "https://www.yudelei.com/",
"contributions": [
"code"
]
},
{
"login": "cendai-mis",
"name": "cendai-mis",
"avatar_url": "https://avatars.githubusercontent.com/u/198525698?v=4",
"profile": "https://github.com/cendai-mis",
"contributions": [
"code"
]
},
{
"login": "smcpeck",
"name": "Shaun McPeck",
"avatar_url": "https://avatars.githubusercontent.com/u/8724583?v=4",
"profile": "https://smcpeck.github.io/",
"contributions": [
"code"
]
},
{
"login": "snazy2000",
"name": "Stephen",
"avatar_url": "https://avatars.githubusercontent.com/u/1378836?v=4",
"profile": "https://github.com/snazy2000",
"contributions": [
"code"
]
},
{
"login": "Nevets82",
"name": "Steven",
"avatar_url": "https://avatars.githubusercontent.com/u/4462739?v=4",
"profile": "http://nevets82.github.io/",
"contributions": [
"code"
]
},
{
"login": "Mateus-Romera",
"name": "Mateus Villar",
"avatar_url": "https://avatars.githubusercontent.com/u/29017267?v=4",
"profile": "https://mateusvillar.com/",
"contributions": [
"code"
]
},
{
"login": "mzack5020",
"name": "Matthew Zackschewski",
"avatar_url": "https://avatars.githubusercontent.com/u/12749393?v=4",
"profile": "https://github.com/mzack5020",
"contributions": [
"code"
]
},
{
"login": "firefrei",
"name": "Matthias Frei",
"avatar_url": "https://avatars.githubusercontent.com/u/12660103?v=4",
"profile": "https://www.frei.media/",
"contributions": [
"code"
]
},
{
"login": "nticaric",
"name": "Nenad Ticaric",
"avatar_url": "https://avatars.githubusercontent.com/u/824840?v=4",
"profile": "https://github.com/nticaric",
"contributions": [
"code"
]
},
{
"login": "Scorcher",
"name": "Nikolay Didenko",
"avatar_url": "https://avatars.githubusercontent.com/u/706439?v=4",
"profile": "https://github.com/Scorcher",
"contributions": [
"code"
]
},
{
"login": "nunomaduro",
"name": "Nuno Maduro",
"avatar_url": "https://avatars.githubusercontent.com/u/5457236?v=4",
"profile": "https://nunomaduro.com/sponsorships",
"contributions": [
"code"
]
},
{
"login": "owalerys",
"name": "Oliver Walerys",
"avatar_url": "https://avatars.githubusercontent.com/u/8883074?v=4",
"profile": "https://tektikhq.com/",
"contributions": [
"code"
]
},
{
"login": "rcmcdonald91",
"name": "R. Christian McDonald",
"avatar_url": "https://avatars.githubusercontent.com/u/3102039?v=4",
"profile": "https://keybase.io/rcmcdonald91",
"contributions": [
"code"
]
},
{
"login": "nixn",
"name": "nix",
"avatar_url": "https://avatars.githubusercontent.com/u/1525581?v=4",
"profile": "https://nnix.net/",
"contributions": [
"code"
]
},
{
"login": "octobunny",
"name": "octobunny",
"avatar_url": "https://avatars.githubusercontent.com/u/55462380?v=4",
"profile": "https://github.com/octobunny",
"contributions": [
"code"
]
},
{
"login": "sreyemnayr",
"name": "Ryan",
"avatar_url": "https://avatars.githubusercontent.com/u/8558670?v=4",
"profile": "https://github.com/sreyemnayr",
"contributions": [
"code"
]
},
{
"login": "p3nj",
"name": "p3nj",
"avatar_url": "https://avatars.githubusercontent.com/u/1501022?v=4",
"profile": "https://benji.ltd/",
"contributions": [
"code"
]
},
{
"login": "timwsuqld",
"name": "Tim White",
"avatar_url": "https://avatars.githubusercontent.com/u/6201617?v=4",
"profile": "https://github.com/timwsuqld",
"contributions": [
"code"
]
},
{
"login": "yannikp",
"name": "yannikp",
"avatar_url": "https://avatars.githubusercontent.com/u/22473767?v=4",
"profile": "https://github.com/yannikp",
"contributions": [
"code"
]
},
{
"login": "viclou",
"name": "victoria",
"avatar_url": "https://avatars.githubusercontent.com/u/20525448?v=4",
"profile": "https://github.com/viclou",
"contributions": [
"code"
]
},
{
"login": "valentyntu",
"name": "Valentyn Tulub",
"avatar_url": "https://avatars.githubusercontent.com/u/40685314?v=4",
"profile": "https://github.com/valentyntu",
"contributions": [
"code"
]
},
{
"login": "Wouter0100",
"name": "Wouter van Os",
"avatar_url": "https://avatars.githubusercontent.com/u/864520?v=4",
"profile": "http://wouter0100.nl/",
"contributions": [
"code"
]
},
{
"login": "xWyatt",
"name": "Wyatt Teeter",
"avatar_url": "https://avatars.githubusercontent.com/u/3946540?v=4",
"profile": "https://www.linkedin.com/in/wyatt-teeter",
"contributions": [
"code"
]
},
{
"login": "terwey",
"name": "Yorick Terweijden",
"avatar_url": "https://avatars.githubusercontent.com/u/1596124?v=4",
"profile": "https://github.com/terwey",
"contributions": [
"code"
]
},
{
"login": "bmkalle",
"name": "bmkalle",
"avatar_url": "https://avatars.githubusercontent.com/u/69298836?v=4",
"profile": "https://github.com/bmkalle",
"contributions": [
"code"
]
},
{
"login": "bricelabelle",
"name": "bricelabelle",
"avatar_url": "https://avatars.githubusercontent.com/u/28403467?v=4",
"profile": "https://github.com/bricelabelle",
"contributions": [
"code"
]
},
{
"login": "corydlamb",
"name": "corydlamb",
"avatar_url": "https://avatars.githubusercontent.com/u/97770090?v=4",
"profile": "https://github.com/corydlamb",
"contributions": [
"code"
]
},
{
"login": "splashx",
"name": "Diogenes S. Jesus",
"avatar_url": "https://avatars.githubusercontent.com/u/1154133?v=4",
"profile": "http://twitter.com/splash",
"contributions": [
"code"
]
},
{
"login": "dkmansion",
"name": "D M",
"avatar_url": "https://avatars.githubusercontent.com/u/5826629?v=4",
"profile": "https://github.com/dkmansion",
"contributions": [
"code"
]
},
{
"login": "Jarli01",
"name": "Dustin B",
"avatar_url": "https://avatars.githubusercontent.com/u/14837699?v=4",
"profile": "https://github.com/Jarli01",
"contributions": [
"code"
]
},
{
"login": "fabiang",
"name": "Fabian Grutschus",
"avatar_url": "https://avatars.githubusercontent.com/u/348344?v=4",
"profile": "https://github.com/fabiang",
"contributions": [
"code"
]
},
{
"login": "MelonSmasher",
"name": "MelonSmasher",
"avatar_url": "https://avatars.githubusercontent.com/u/1491053?v=4",
"profile": "https://github.com/MelonSmasher",
"contributions": [
"code"
]
},
{
"login": "AlexanderWPapyrus",
"name": "AlexanderWPapyrus",
"avatar_url": "https://avatars.githubusercontent.com/u/80526133?v=4",
"profile": "https://github.com/AlexanderWPapyrus",
"contributions": [
"code"
]
},
{
"login": "disc",
"name": "Alexandr Hacicheant",
"avatar_url": "https://avatars.githubusercontent.com/u/306231?v=4",
"profile": "https://github.com/disc",
"contributions": [
"code"
]
},
{
"login": "hex128",
"name": "Hex",
"avatar_url": "https://avatars.githubusercontent.com/u/3032891?v=4",
"profile": "https://hex128.io/",
"contributions": [
"code"
]
},
{
"login": "arukompas",
"name": "Arunas Skirius",
"avatar_url": "https://avatars.githubusercontent.com/u/8697942?v=4",
"profile": "https://github.com/arukompas",
"contributions": [
"code"
]
},
{
"login": "benperiton",
"name": "Ben Periton",
"avatar_url": "https://avatars.githubusercontent.com/u/104396?v=4",
"profile": "https://github.com/benperiton",
"contributions": [
"code"
]
},
{
"login": "byronwolfman",
"name": "Byron Wolfman",
"avatar_url": "https://avatars.githubusercontent.com/u/11906832?v=4",
"profile": "https://wolfman.dev/",
"contributions": [
"code"
]
},
{
"login": "CalvinSchwartz",
"name": "Calvin",
"avatar_url": "https://avatars.githubusercontent.com/u/56485508?v=4",
"profile": "https://github.com/CalvinSchwartz",
"contributions": [
"code"
]
},
{
"login": "juanfont",
"name": "Juan Font",
"avatar_url": "https://avatars.githubusercontent.com/u/181059?v=4",
"profile": "https://github.com/juanfont",
"contributions": [
"code"
]
},
{
"login": "juhotaipale",
"name": "Juho Taipale",
"avatar_url": "https://avatars.githubusercontent.com/u/13137708?v=4",
"profile": "https://github.com/juhotaipale",
"contributions": [
"code"
]
},
{
"login": "KorvinSzanto",
"name": "Korvin Szanto",
"avatar_url": "https://avatars.githubusercontent.com/u/1007419?v=4",
"profile": "https://github.com/KorvinSzanto",
"contributions": [
"code"
]
},
{
"login": "sniff122",
"name": "Lewis Foster",
"avatar_url": "https://avatars.githubusercontent.com/u/8513053?v=4",
"profile": "https://lewisfoster.foo/",
"contributions": [
"code"
]
},
{
"login": "loganswartz",
"name": "Logan Swartzendruber",
"avatar_url": "https://avatars.githubusercontent.com/u/33877541?v=4",
"profile": "https://github.com/loganswartz",
"contributions": [
"code"
]
},
{
"login": "lopezio",
"name": "Lorenzo P.",
"avatar_url": "https://avatars.githubusercontent.com/u/1156208?v=4",
"profile": "https://github.com/lopezio",
"contributions": [
"code"
]
},
{
"login": "m4us1ne",
"name": "Lukas Jung",
"avatar_url": "https://avatars.githubusercontent.com/u/33946590?v=4",
"profile": "https://github.com/m4us1ne",
"contributions": [
"code"
]
},
{
"login": "LeafedFox",
"name": "Ellie",
"avatar_url": "https://avatars.githubusercontent.com/u/10965027?v=4",
"profile": "https://leafedfox.xyz/",
"contributions": [
"code"
]
},
{
"login": "gastamper",
"name": "GA Stamper",
"avatar_url": "https://avatars.githubusercontent.com/u/20960555?v=4",
"profile": "https://github.com/gastamper",
"contributions": [
"code"
]
},
{
"login": "gl-pup",
"name": "Guillaume Lefranc",
"avatar_url": "https://avatars.githubusercontent.com/u/206553556?v=4",
"profile": "https://github.com/gl-pup",
"contributions": [
"code"
]
},
{
"login": "dasjoe",
"name": "Hajo Möller",
"avatar_url": "https://avatars.githubusercontent.com/u/733892?v=4",
"profile": "https://github.com/dasjoe",
"contributions": [
"code"
]
},
{
"login": "pottom",
"name": "Istvan Basa",
"avatar_url": "https://avatars.githubusercontent.com/u/3420063?v=4",
"profile": "https://github.com/pottom",
"contributions": [
"code"
]
},
{
"login": "jjasghar",
"name": "JJ Asghar",
"avatar_url": "https://avatars.githubusercontent.com/u/810824?v=4",
"profile": "https://jjasghar.github.io/",
"contributions": [
"code"
]
},
{
"login": "JemCdo",
"name": "James E. Msenga",
"avatar_url": "https://avatars.githubusercontent.com/u/40404495?v=4",
"profile": "https://github.com/JemCdo",
"contributions": [
"code"
]
},
{
"login": "jfwiebe",
"name": "Jan Felix Wiebe",
"avatar_url": "https://avatars.githubusercontent.com/u/6865786?v=4",
"profile": "https://github.com/jfwiebe",
"contributions": [
"code"
]
},
{
"login": "drexljo",
"name": "Jo Drexl",
"avatar_url": "https://avatars.githubusercontent.com/u/43412008?v=4",
"profile": "https://www.nfon.com/",
"contributions": [
"code"
]
},
{
"login": "austinsasko",
"name": "Austin Sasko",
"avatar_url": "https://avatars.githubusercontent.com/u/4807843?v=4",
"profile": "https://github.com/austinsasko",
"contributions": [
"code"
]
},
{
"login": "JassonCordones",
"name": "Jasson",
"avatar_url": "https://avatars.githubusercontent.com/u/4875039?v=4",
"profile": "http://jassoncordones.github.io",
"contributions": [
"code"
]
},
{
"login": "Tinyblargon",
"name": "Okean",
"avatar_url": "https://avatars.githubusercontent.com/u/76069640?v=4",
"profile": "https://github.com/Tinyblargon",
"contributions": [
"code"
]
},
{
"login": "amedranogil",
"name": "Alejandro Medrano",
"avatar_url": "https://avatars.githubusercontent.com/u/6515064?v=4",
"profile": "https://www.lst.tfo.upm.es/alejandro-medrano/",
"contributions": [
"code"
]
},
{
"login": "lukaskraic",
"name": "Lukas Kraic",
"avatar_url": "https://avatars.githubusercontent.com/u/58696401?v=4",
"profile": "https://github.com/lukaskraic",
"contributions": [
"code"
]
}
]
}

View File

@@ -10,10 +10,10 @@ name: Codacy Security Scan
on:
push:
branches: [ master ]
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [ develop ]
schedule:
- cron: '36 23 * * 3'

View File

@@ -1,5 +1,5 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker Intel/amd64 images (Ubuntu)
# Snipe-IT (Alpine) Docker image build for hub.docker.com
name: Docker images (Alpine)
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
@@ -19,72 +19,7 @@ permissions:
contents: read
jobs:
docker-ubuntu-intel:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'grokability/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: grokability/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
docker-alpine-intel:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-latest
@@ -123,7 +58,7 @@ jobs:
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'grokability/snipe-it' image
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
@@ -131,7 +66,7 @@ jobs:
id: meta_build
uses: docker/metadata-action@v5
with:
images: grokability/snipe-it
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
@@ -142,7 +77,7 @@ jobs:
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -1,151 +0,0 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker ARM64 images (Ubuntu)
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
docker-ubuntu-arm:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-24.04-arm
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'grokability/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: grokability/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
docker-alpine-arm:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-24.04-arm
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=tag,suffix=-alpine
type=semver,pattern=v{{major}}-latest-alpine
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'grokability/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: grokability/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}

86
.github/workflows/docker-ubuntu.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker images (Ubuntu)
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}

View File

@@ -1,60 +1,73 @@
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
| [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/grokability/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/grokability/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/grokability/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/grokability/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/grokability/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vjandrea "Code") |
| :---: | :---: | :---: |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| :---: |
| [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/grokability/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/grokability/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/grokability/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
| [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/grokability/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/grokability/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/grokability/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/grokability/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/grokability/snipe-it/commits?author=rassie "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/grokability/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/grokability/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/grokability/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/grokability/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/grokability/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/grokability/snipe-it/commits?author=adamdunson "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/5547470?v=3" width="110px;"/><br /><sub>Hereward</sub>](https://github.com/thehereward)<br />[💻](https://github.com/grokability/snipe-it/commits?author=thehereward "Code") | [<img src="https://avatars0.githubusercontent.com/u/5802977?v=3" width="110px;"/><br /><sub>swoopdk</sub>](https://github.com/swoopdk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=swoopdk "Code") | [<img src="https://avatars1.githubusercontent.com/u/3470403?v=3" width="110px;"/><br /><sub>Abdullah Alansari</sub>](https://linkedin.com/in/ahimta)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Ahimta "Code") | [<img src="https://avatars0.githubusercontent.com/u/796443?v=3" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[💻](https://github.com/grokability/snipe-it/commits?author=MicaelRodrigues "Code") | [<img src="https://avatars0.githubusercontent.com/u/614564?v=3" width="110px;"/><br /><sub>Patrick Gallagher</sub>](http://macadmincorner.com)<br />[📖](https://github.com/grokability/snipe-it/commits?author=patgmac "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/7165922?v=3" width="110px;"/><br /><sub>Miliamber</sub>](https://github.com/Miliamber)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Miliamber "Code") | [<img src="https://avatars3.githubusercontent.com/u/861766?v=3" width="110px;"/><br /><sub>hawk554</sub>](https://github.com/hawk554)<br />[💻](https://github.com/grokability/snipe-it/commits?author=hawk554 "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1695622?v=3" width="110px;"/><br /><sub>Justin Kerr</sub>](http://jbirdkerr.net)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jbirdkerr "Code") | [<img src="https://avatars3.githubusercontent.com/u/11426176?v=3" width="110px;"/><br /><sub>Ira W. Snyder</sub>](http://www.irasnyder.com/devel/)<br />[📖](https://github.com/grokability/snipe-it/commits?author=irasnyd "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/2475759?v=3" width="110px;"/><br /><sub>Aladin Alaily</sub>](https://github.com/aalaily)<br />[💻](https://github.com/grokability/snipe-it/commits?author=aalaily "Code") | [<img src="https://avatars0.githubusercontent.com/u/10247644?v=3" width="110px;"/><br /><sub>Chase Hansen</sub>](https://github.com/kobie-chasehansen)<br />[💻](https://github.com/grokability/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [<img src="https://avatars2.githubusercontent.com/u/13545400?v=3" width="110px;"/><br /><sub>IDM Helpdesk</sub>](https://github.com/IDM-Helpdesk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=IDM-Helpdesk "Code") | [<img src="https://avatars2.githubusercontent.com/u/614439?v=3" width="110px;"/><br /><sub>Kai</sub>](http://balticer.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=balticer "Code") | [<img src="https://avatars1.githubusercontent.com/u/8762511?v=3" width="110px;"/><br /><sub>Michael Daniels</sub>](http://www.michaeldaniels.me)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mdaniels5757 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/1532660?v=3" width="110px;"/><br /><sub>Tom Castleman</sub>](http://tomcastleman.me)<br />[💻](https://github.com/grokability/snipe-it/commits?author=tomcastleman "Code") | [<img src="https://avatars3.githubusercontent.com/u/10723243?v=3" width="110px;"/><br /><sub>Daniel Nemanic</sub>](https://github.com/DanielNemanic)<br />[💻](https://github.com/grokability/snipe-it/commits?author=DanielNemanic "Code") | [<img src="https://avatars0.githubusercontent.com/u/150648?v=3" width="110px;"/><br /><sub>SouthWolf</sub>](https://github.com/southwolf)<br />[💻](https://github.com/grokability/snipe-it/commits?author=southwolf "Code") | [<img src="https://avatars2.githubusercontent.com/u/131616?v=3" width="110px;"/><br /><sub>Ivar Nesje</sub>](https://github.com/ivarne)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ivarne "Code") | [<img src="https://avatars1.githubusercontent.com/u/62333?v=3" width="110px;"/><br /><sub>Jérémy Benoist</sub>](http://www.j0k3r.net)<br />[📖](https://github.com/grokability/snipe-it/commits?author=j0k3r "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/724344?v=3" width="110px;"/><br /><sub>Chris Leathley</sub>](https://github.com/cleathley)<br />[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars0.githubusercontent.com/u/972498?v=3" width="110px;"/><br /><sub>splaer</sub>](https://github.com/splaer)<br />[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/grokability/snipe-it/commits?author=splaer "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/967362?v=3" width="110px;"/><br /><sub>Joe Ferguson</sub>](http://www.joeferguson.me)<br />[💻](https://github.com/grokability/snipe-it/commits?author=svpernova09 "Code") | [<img src="https://avatars3.githubusercontent.com/u/6108682?v=3" width="110px;"/><br /><sub>diwanicki</sub>](https://github.com/diwanicki)<br />[💻](https://github.com/grokability/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/grokability/snipe-it/commits?author=diwanicki "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/2527115?v=3" width="110px;"/><br /><sub>Lee Thoong Ching</sub>](https://github.com/pakkua80)<br />[📖](https://github.com/grokability/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/grokability/snipe-it/commits?author=pakkua80 "Code") | [<img src="https://avatars1.githubusercontent.com/u/461491?v=3" width="110px;"/><br /><sub>Marek Šuppa</sub>](http://shu.io)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mrshu "Code") | [<img src="https://avatars1.githubusercontent.com/u/8693762?v=3" width="110px;"/><br /><sub>Juan J. Martinez</sub>](https://github.com/mizar1616)<br />[🌍](#translation-mizar1616 "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1458388?v=3" width="110px;"/><br /><sub>R Ryan Dial</sub>](https://github.com/rrdial)<br />[🌍](#translation-rrdial "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2871745?v=3" width="110px;"/><br /><sub>Andrej Manduch</sub>](https://github.com/burlito)<br />[📖](https://github.com/grokability/snipe-it/commits?author=burlito "Documentation") |
| [<img src="https://avatars0.githubusercontent.com/u/8341172?v=3" width="110px;"/><br /><sub>Jay Richards</sub>](http://www.cordeos.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=technogenus "Code") | [<img src="https://avatars2.githubusercontent.com/u/7295127?v=3" width="110px;"/><br /><sub>Alexander Innes</sub>](https://necurity.co.uk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=leostat "Code") | [<img src="https://avatars2.githubusercontent.com/u/334485?v=3" width="110px;"/><br /><sub>Danny Garcia</sub>](https://buzzedword.codes)<br />[💻](https://github.com/grokability/snipe-it/commits?author=buzzedword "Code") | [<img src="https://avatars2.githubusercontent.com/u/366855?v=3" width="110px;"/><br /><sub>archpoint</sub>](https://github.com/archpoint)<br />[💻](https://github.com/grokability/snipe-it/commits?author=archpoint "Code") | [<img src="https://avatars1.githubusercontent.com/u/67991?v=3" width="110px;"/><br /><sub>Jake McGraw</sub>](http://www.jakemcgraw.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jakemcgraw "Code") | [<img src="https://avatars1.githubusercontent.com/u/1714374?v=3" width="110px;"/><br /><sub>FleischKarussel</sub>](https://github.com/FleischKarussel)<br />[📖](https://github.com/grokability/snipe-it/commits?author=FleischKarussel "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/319644?v=3" width="110px;"/><br /><sub>Dylan Yi</sub>](https://github.com/feeva)<br />[💻](https://github.com/grokability/snipe-it/commits?author=feeva "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/857740?v=3" width="110px;"/><br /><sub>Gil Rutkowski</sub>](http://FlashingCursor.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=flashingcursor "Code") | [<img src="https://avatars3.githubusercontent.com/u/129360?v=3" width="110px;"/><br /><sub>Desmond Morris</sub>](http://www.desmondmorris.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=desmondmorris "Code") | [<img src="https://avatars2.githubusercontent.com/u/52936?v=3" width="110px;"/><br /><sub>Nick Peelman</sub>](http://peelman.us)<br />[💻](https://github.com/grokability/snipe-it/commits?author=peelman "Code") | [<img src="https://avatars0.githubusercontent.com/u/53161?v=3" width="110px;"/><br /><sub>Abraham Vegh</sub>](https://abrahamvegh.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=abrahamvegh "Code") | [<img src="https://avatars0.githubusercontent.com/u/2818680?v=3" width="110px;"/><br /><sub>Mohamed Rashid</sub>](https://github.com/rashivkp)<br />[📖](https://github.com/grokability/snipe-it/commits?author=rashivkp "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/1509456?v=3" width="110px;"/><br /><sub>Kasey</sub>](http://hinchk.github.io)<br />[💻](https://github.com/grokability/snipe-it/commits?author=HinchK "Code") | [<img src="https://avatars2.githubusercontent.com/u/10522541?v=3" width="110px;"/><br /><sub>Brett</sub>](https://github.com/BrettFagerlund)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=BrettFagerlund "Tests") |
| [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/grokability/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/grokability/snipe-it/commits?author=gibsonjoshua55 "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/grokability/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/grokability/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/grokability/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/grokability/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/grokability/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/grokability/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/grokability/snipe-it/commits?author=BlueHatbRit "Documentation") |
| [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/grokability/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/grokability/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/grokability/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/grokability/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/grokability/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vcordes79 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/grokability/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/grokability/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/grokability/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/grokability/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/grokability/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/grokability/snipe-it/commits?author=hannahtinkler "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/grokability/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/grokability/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/grokability/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/grokability/snipe-it/commits?author=plexorama "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/grokability/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/grokability/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
| [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/4930051?v=4" width="110px;"/><br /><sub>Wes Hulette</sub>](http://macfoo.wordpress.com/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jwhulette "Code") | [<img src="https://avatars0.githubusercontent.com/u/8134591?v=4" width="110px;"/><br /><sub>patrict</sub>](https://github.com/patrict)<br />[💻](https://github.com/grokability/snipe-it/commits?author=patrict "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/2611616?v=4" width="110px;"/><br /><sub>Dmitriy Minaev</sub>](https://github.com/VELIKII-DIVAN)<br />[💻](https://github.com/grokability/snipe-it/commits?author=VELIKII-DIVAN "Code") | [<img src="https://avatars0.githubusercontent.com/u/5132245?v=4" width="110px;"/><br /><sub>liquidhorse</sub>](https://github.com/liquidhorse)<br />[💻](https://github.com/grokability/snipe-it/commits?author=liquidhorse "Code") | [<img src="https://avatars1.githubusercontent.com/u/183678?v=4" width="110px;"/><br /><sub>Jordi Boggiano</sub>](https://seld.be/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Seldaek "Code") | [<img src="https://avatars0.githubusercontent.com/u/653557?v=4" width="110px;"/><br /><sub>Ivan Nieto</sub>](https://github.com/inietov)<br />[💻](https://github.com/grokability/snipe-it/commits?author=inietov "Code") | [<img src="https://avatars2.githubusercontent.com/u/6764151?v=4" width="110px;"/><br /><sub>Ben RUBSON</sub>](https://github.com/benrubson)<br />[💻](https://github.com/grokability/snipe-it/commits?author=benrubson "Code") | [<img src="https://avatars2.githubusercontent.com/u/8554558?v=4" width="110px;"/><br /><sub>NMathar</sub>](https://github.com/NMathar)<br />[💻](https://github.com/grokability/snipe-it/commits?author=NMathar "Code") | [<img src="https://avatars1.githubusercontent.com/u/139566?v=4" width="110px;"/><br /><sub>Steffen</sub>](https://github.com/smb)<br />[💻](https://github.com/grokability/snipe-it/commits?author=smb "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/6609453?v=4" width="110px;"/><br /><sub>Sxderp</sub>](https://github.com/Sxderp)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Sxderp "Code") | [<img src="https://avatars1.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>fanta8897</sub>](https://github.com/fanta8897)<br />[💻](https://github.com/grokability/snipe-it/commits?author=fanta8897 "Code") | [<img src="https://avatars2.githubusercontent.com/u/2576509?v=4" width="110px;"/><br /><sub>Andrey Bolonin</sub>](https://andreybolonin.com/phpconsulting/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=andreybolonin "Code") | [<img src="https://avatars3.githubusercontent.com/u/2173307?v=4" width="110px;"/><br /><sub>shinayoshi</sub>](http://www.shinayoshi.net/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=shinayoshi "Code") | [<img src="https://avatars3.githubusercontent.com/u/2130159?v=4" width="110px;"/><br /><sub>Hubert</sub>](https://github.com/reuser)<br />[💻](https://github.com/grokability/snipe-it/commits?author=reuser "Code") | [<img src="https://avatars0.githubusercontent.com/u/6865789?v=4" width="110px;"/><br /><sub>KeenRivals</sub>](https://brashear.me)<br />[💻](https://github.com/grokability/snipe-it/commits?author=KeenRivals "Code") | [<img src="https://avatars3.githubusercontent.com/u/2902513?v=4" width="110px;"/><br /><sub>omyno</sub>](https://github.com/omyno)<br />[💻](https://github.com/grokability/snipe-it/commits?author=omyno "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/6271335?v=4" width="110px;"/><br /><sub>Evgeny</sub>](https://github.com/jackka)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jackka "Code") | [<img src="https://avatars2.githubusercontent.com/u/1169963?v=4" width="110px;"/><br /><sub>Colin Campbell</sub>](https://digitalist.se)<br />[💻](https://github.com/grokability/snipe-it/commits?author=colin-campbell "Code") | [<img src="https://avatars3.githubusercontent.com/u/2872098?v=4" width="110px;"/><br /><sub>Ľubomír Kučera</sub>](https://github.com/lubo)<br />[💻](https://github.com/grokability/snipe-it/commits?author=lubo "Code") | [<img src="https://avatars3.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://www.sourceguru.net)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Mezzle "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/grokability/snipe-it/commits?author=timothyfarmer "Code") | [<img src="https://avatars0.githubusercontent.com/u/17459600?v=4" width="110px;"/><br /><sub>Marián Skrip</sub>](https://github.com/mskrip)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mskrip "Code") | [<img src="https://avatars2.githubusercontent.com/u/47435081?v=4" width="110px;"/><br /><sub>Godfrey Martinez</sub>](https://github.com/Godmartinz)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Godmartinz "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/2075128?v=4" width="110px;"/><br /><sub>bigtreeEdo</sub>](https://github.com/bigtreeEdo)<br />[💻](https://github.com/grokability/snipe-it/commits?author=bigtreeEdo "Code") | [<img src="https://avatars0.githubusercontent.com/u/5000430?v=4" width="110px;"/><br /><sub>Colin McNeil</sub>](https://colinmcneil.me/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ColinMcNeil "Code") | [<img src="https://avatars0.githubusercontent.com/u/421625?v=4" width="110px;"/><br /><sub>JoKneeMo</sub>](https://github.com/JoKneeMo)<br />[💻](https://github.com/grokability/snipe-it/commits?author=JoKneeMo "Code") | [<img src="https://avatars0.githubusercontent.com/u/54849013?v=4" width="110px;"/><br /><sub>Joshi</sub>](http://www.redbridge.se)<br />[💻](https://github.com/grokability/snipe-it/commits?author=joshi-redbridge "Code") | [<img src="https://avatars2.githubusercontent.com/u/15731458?v=4" width="110px;"/><br /><sub>Anthony Burns</sub>](https://github.com/anthonypburns)<br />[💻](https://github.com/grokability/snipe-it/commits?author=anthonypburns "Code") | [<img src="https://avatars1.githubusercontent.com/u/63399474?v=4" width="110px;"/><br /><sub>johnson-yi</sub>](https://github.com/johnson-yi)<br />[💻](https://github.com/grokability/snipe-it/commits?author=johnson-yi "Code") | [<img src="https://avatars1.githubusercontent.com/u/1862720?v=4" width="110px;"/><br /><sub>Sanjay Govind</sub>](https://tangentmc.net)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sanjay900 "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/1255375?v=4" width="110px;"/><br /><sub>Peter Upfold</sub>](https://peter.upfold.org.uk/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PeterUpfold "Code") | [<img src="https://avatars2.githubusercontent.com/u/961717?v=4" width="110px;"/><br /><sub>Jared Biel</sub>](https://github.com/jbiel)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jbiel "Code") | [<img src="https://avatars1.githubusercontent.com/u/1733625?v=4" width="110px;"/><br /><sub>Dampfklon</sub>](https://github.com/dampfklon)<br />[💻](https://github.com/grokability/snipe-it/commits?author=dampfklon "Code") | [<img src="https://avatars2.githubusercontent.com/u/52973156?v=4" width="110px;"/><br /><sub>Charles Hamilton</sub>](https://communityclosing.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=chamilton-ccn "Code") | [<img src="https://avatars.githubusercontent.com/u/551789?v=4" width="110px;"/><br /><sub>Giuseppe Iannello</sub>](https://github.com/giannello)<br />[💻](https://github.com/grokability/snipe-it/commits?author=giannello "Code") | [<img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="110px;"/><br /><sub>Peter Dave Hello</sub>](https://www.peterdavehello.org/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PeterDaveHello "Code") | [<img src="https://avatars.githubusercontent.com/u/6106332?v=4" width="110px;"/><br /><sub>sigmoidal</sub>](https://github.com/sigmoidal)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sigmoidal "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2082554?v=4" width="110px;"/><br /><sub>Vincent Lainé</sub>](https://github.com/phenixdotnet)<br />[💻](https://github.com/grokability/snipe-it/commits?author=phenixdotnet "Code") | [<img src="https://avatars.githubusercontent.com/u/1943040?v=4" width="110px;"/><br /><sub>Lucas Pleß</sub>](http://www.lucas-pless.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=derlucas "Code") | [<img src="https://avatars.githubusercontent.com/u/472804?v=4" width="110px;"/><br /><sub>Ian Littman</sub>](http://twitter.com/iansltx)<br />[💻](https://github.com/grokability/snipe-it/commits?author=iansltx "Code") | [<img src="https://avatars.githubusercontent.com/u/3519029?v=4" width="110px;"/><br /><sub>João Paulo</sub>](https://github.com/PauloLuna)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PauloLuna "Code") | [<img src="https://avatars.githubusercontent.com/u/70443365?v=4" width="110px;"/><br /><sub>ThoBur</sub>](https://github.com/ThoBur)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ThoBur "Code") | [<img src="https://avatars.githubusercontent.com/u/1972329?v=4" width="110px;"/><br /><sub>Alexander Chibrikin</sub>](http://phpprofi.ru/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=alek13 "Code") | [<img src="https://avatars.githubusercontent.com/u/438332?v=4" width="110px;"/><br /><sub>Anthony Winstanley</sub>](https://github.com/winstan)<br />[💻](https://github.com/grokability/snipe-it/commits?author=winstan "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3075214?v=4" width="110px;"/><br /><sub>Folke</sub>](https://github.com/fashberg)<br />[💻](https://github.com/grokability/snipe-it/commits?author=fashberg "Code") | [<img src="https://avatars.githubusercontent.com/u/1351571?v=4" width="110px;"/><br /><sub>Bennett Blodinger</sub>](https://github.com/benwa)<br />[💻](https://github.com/grokability/snipe-it/commits?author=benwa "Code") | [<img src="https://avatars.githubusercontent.com/u/2974631?v=4" width="110px;"/><br /><sub>NMC</sub>](https://nmc.dev)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ncareau "Code") | [<img src="https://avatars.githubusercontent.com/u/52182449?v=4" width="110px;"/><br /><sub>andres-baller</sub>](https://github.com/andres-baller)<br />[💻](https://github.com/grokability/snipe-it/commits?author=andres-baller "Code") | [<img src="https://avatars.githubusercontent.com/u/67109348?v=4" width="110px;"/><br /><sub>sean-borg</sub>](https://github.com/sean-borg)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sean-borg "Code") | [<img src="https://avatars.githubusercontent.com/u/32170051?v=4" width="110px;"/><br /><sub>EDVLeer</sub>](https://github.com/EDVLeer)<br />[💻](https://github.com/grokability/snipe-it/commits?author=EDVLeer "Code") | [<img src="https://avatars.githubusercontent.com/u/23075196?v=4" width="110px;"/><br /><sub>Kurokat</sub>](https://github.com/Kurokat)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Kurokat "Code") |
| [<img src="https://avatars.githubusercontent.com/u/915514?v=4" width="110px;"/><br /><sub>Kevin Köllmann</sub>](https://www.kevinkoellmann.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=koelle25 "Code") | [<img src="https://avatars.githubusercontent.com/u/49025941?v=4" width="110px;"/><br /><sub>sw-mreyes</sub>](https://github.com/sw-mreyes)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sw-mreyes "Code") | [<img src="https://avatars.githubusercontent.com/u/70129?v=4" width="110px;"/><br /><sub>Joel Pittet</sub>](https://pittet.ca)<br />[💻](https://github.com/grokability/snipe-it/commits?author=joelpittet "Code") | [<img src="https://avatars.githubusercontent.com/u/792695?v=4" width="110px;"/><br /><sub>Eli Young</sub>](https://elyscape.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=elyscape "Code") | [<img src="https://avatars.githubusercontent.com/u/317015?v=4" width="110px;"/><br /><sub>Raell Dottin</sub>](https://github.com/raelldottin)<br />[💻](https://github.com/grokability/snipe-it/commits?author=raelldottin "Code") | [<img src="https://avatars.githubusercontent.com/u/1446856?v=4" width="110px;"/><br /><sub>Tom Misilo</sub>](https://github.com/misilot)<br />[💻](https://github.com/grokability/snipe-it/commits?author=misilot "Code") | [<img src="https://avatars.githubusercontent.com/u/4496300?v=4" width="110px;"/><br /><sub>David Davenne</sub>](http://david.davenne.be)<br />[💻](https://github.com/grokability/snipe-it/commits?author=JuustoMestari "Code") |
| [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/grokability/snipe-it/commits?author=limeless "Code") |
| [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/grokability/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/grokability/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=psarossy "Code") |
| [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/grokability/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/grokability/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/grokability/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/grokability/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/grokability/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=travismiller "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/grokability/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/grokability/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jerm "Code") |
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/grokability/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/grokability/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=leitwerk-ag "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/grokability/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/grokability/snipe-it/commits?author=UniversalSuperBox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/grokability/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/grokability/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/grokability/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/grokability/snipe-it/commits?author=QveenSi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/grokability/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/grokability/snipe-it/commits?author=QveenSi "Code") |
| [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/grokability/snipe-it/commits?author=chrisweirich "Code") | [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/grokability/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vickyjaura183 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/grokability/snipe-it/commits?author=julian-piehl "Code") | [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/grokability/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/grokability/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/grokability/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Vautia "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/grokability/snipe-it/commits?author=chartjes "Code") | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/grokability/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/grokability/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/grokability/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/grokability/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/grokability/snipe-it/commits?author=fernando-almeida "Code") |
| [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/grokability/snipe-it/commits?author=akemidx "Code") | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/grokability/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/grokability/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/grokability/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/grokability/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/grokability/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/grokability/snipe-it/commits?author=floschoepfer "Code") |
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/grokability/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/grokability/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/grokability/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/grokability/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ak-piracha "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/grokability/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/grokability/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/grokability/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/grokability/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Azooz2014 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/grokability/snipe-it/commits?author=bilias "Code") | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/grokability/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/grokability/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/grokability/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Q4kK "Code") |
| [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/grokability/snipe-it/commits?author=squintfox "Code") | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/grokability/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/grokability/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/grokability/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/grokability/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/grokability/snipe-it/commits?author=bryanlopezinc "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/grokability/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/grokability/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/grokability/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/grokability/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/grokability/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/grokability/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/grokability/snipe-it/commits?author=arne-kroeger "Code") |
| [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Glukose1 "Code") | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/grokability/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/grokability/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/grokability/snipe-it/commits?author=sgross-emlix "Code") |
| [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/grokability/snipe-it/commits?author=AnouarTouati "Code") | [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/grokability/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/grokability/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/grokability/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/grokability/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/grokability/snipe-it/commits?author=Fiala06 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/grokability/snipe-it/commits?author=ntaylor-86 "Code") | [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/grokability/snipe-it/commits?author=fvollmer "Code") |[<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/grokability/snipe-it/commits?author=36864 "Code") |[<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>CloCkWeRX</sub>](https://github.com/CloCkWeRX)<br />[💻](https://github.com/grokability/snipe-it/commits?author=CloCkWeRX "Code")|[<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/grokability/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/grokability/snipe-it/commits?author=mrdahbi "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
| [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/5547470?v=3" width="110px;"/><br /><sub>Hereward</sub>](https://github.com/thehereward)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thehereward "Code") | [<img src="https://avatars0.githubusercontent.com/u/5802977?v=3" width="110px;"/><br /><sub>swoopdk</sub>](https://github.com/swoopdk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=swoopdk "Code") | [<img src="https://avatars1.githubusercontent.com/u/3470403?v=3" width="110px;"/><br /><sub>Abdullah Alansari</sub>](https://linkedin.com/in/ahimta)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Ahimta "Code") | [<img src="https://avatars0.githubusercontent.com/u/796443?v=3" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues "Code") | [<img src="https://avatars0.githubusercontent.com/u/614564?v=3" width="110px;"/><br /><sub>Patrick Gallagher</sub>](http://macadmincorner.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=patgmac "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/7165922?v=3" width="110px;"/><br /><sub>Miliamber</sub>](https://github.com/Miliamber)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Miliamber "Code") | [<img src="https://avatars3.githubusercontent.com/u/861766?v=3" width="110px;"/><br /><sub>hawk554</sub>](https://github.com/hawk554)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hawk554 "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1695622?v=3" width="110px;"/><br /><sub>Justin Kerr</sub>](http://jbirdkerr.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbirdkerr "Code") | [<img src="https://avatars3.githubusercontent.com/u/11426176?v=3" width="110px;"/><br /><sub>Ira W. Snyder</sub>](http://www.irasnyder.com/devel/)<br />[📖](https://github.com/snipe/snipe-it/commits?author=irasnyd "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/2475759?v=3" width="110px;"/><br /><sub>Aladin Alaily</sub>](https://github.com/aalaily)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aalaily "Code") | [<img src="https://avatars0.githubusercontent.com/u/10247644?v=3" width="110px;"/><br /><sub>Chase Hansen</sub>](https://github.com/kobie-chasehansen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [<img src="https://avatars2.githubusercontent.com/u/13545400?v=3" width="110px;"/><br /><sub>IDM Helpdesk</sub>](https://github.com/IDM-Helpdesk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk "Code") | [<img src="https://avatars2.githubusercontent.com/u/614439?v=3" width="110px;"/><br /><sub>Kai</sub>](http://balticer.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balticer "Code") | [<img src="https://avatars1.githubusercontent.com/u/8762511?v=3" width="110px;"/><br /><sub>Michael Daniels</sub>](http://www.michaeldaniels.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mdaniels5757 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/1532660?v=3" width="110px;"/><br /><sub>Tom Castleman</sub>](http://tomcastleman.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tomcastleman "Code") | [<img src="https://avatars3.githubusercontent.com/u/10723243?v=3" width="110px;"/><br /><sub>Daniel Nemanic</sub>](https://github.com/DanielNemanic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielNemanic "Code") | [<img src="https://avatars0.githubusercontent.com/u/150648?v=3" width="110px;"/><br /><sub>SouthWolf</sub>](https://github.com/southwolf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=southwolf "Code") | [<img src="https://avatars2.githubusercontent.com/u/131616?v=3" width="110px;"/><br /><sub>Ivar Nesje</sub>](https://github.com/ivarne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ivarne "Code") | [<img src="https://avatars1.githubusercontent.com/u/62333?v=3" width="110px;"/><br /><sub>Jérémy Benoist</sub>](http://www.j0k3r.net)<br />[📖](https://github.com/snipe/snipe-it/commits?author=j0k3r "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/724344?v=3" width="110px;"/><br /><sub>Chris Leathley</sub>](https://github.com/cleathley)<br />[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars0.githubusercontent.com/u/972498?v=3" width="110px;"/><br /><sub>splaer</sub>](https://github.com/splaer)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=splaer "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/967362?v=3" width="110px;"/><br /><sub>Joe Ferguson</sub>](http://www.joeferguson.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=svpernova09 "Code") | [<img src="https://avatars3.githubusercontent.com/u/6108682?v=3" width="110px;"/><br /><sub>diwanicki</sub>](https://github.com/diwanicki)<br />[💻](https://github.com/snipe/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/snipe/snipe-it/commits?author=diwanicki "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/2527115?v=3" width="110px;"/><br /><sub>Lee Thoong Ching</sub>](https://github.com/pakkua80)<br />[📖](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Code") | [<img src="https://avatars1.githubusercontent.com/u/461491?v=3" width="110px;"/><br /><sub>Marek Šuppa</sub>](http://shu.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrshu "Code") | [<img src="https://avatars1.githubusercontent.com/u/8693762?v=3" width="110px;"/><br /><sub>Juan J. Martinez</sub>](https://github.com/mizar1616)<br />[🌍](#translation-mizar1616 "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1458388?v=3" width="110px;"/><br /><sub>R Ryan Dial</sub>](https://github.com/rrdial)<br />[🌍](#translation-rrdial "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2871745?v=3" width="110px;"/><br /><sub>Andrej Manduch</sub>](https://github.com/burlito)<br />[📖](https://github.com/snipe/snipe-it/commits?author=burlito "Documentation") |
| [<img src="https://avatars0.githubusercontent.com/u/8341172?v=3" width="110px;"/><br /><sub>Jay Richards</sub>](http://www.cordeos.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=technogenus "Code") | [<img src="https://avatars2.githubusercontent.com/u/7295127?v=3" width="110px;"/><br /><sub>Alexander Innes</sub>](https://necurity.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leostat "Code") | [<img src="https://avatars2.githubusercontent.com/u/334485?v=3" width="110px;"/><br /><sub>Danny Garcia</sub>](https://buzzedword.codes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=buzzedword "Code") | [<img src="https://avatars2.githubusercontent.com/u/366855?v=3" width="110px;"/><br /><sub>archpoint</sub>](https://github.com/archpoint)<br />[💻](https://github.com/snipe/snipe-it/commits?author=archpoint "Code") | [<img src="https://avatars1.githubusercontent.com/u/67991?v=3" width="110px;"/><br /><sub>Jake McGraw</sub>](http://www.jakemcgraw.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jakemcgraw "Code") | [<img src="https://avatars1.githubusercontent.com/u/1714374?v=3" width="110px;"/><br /><sub>FleischKarussel</sub>](https://github.com/FleischKarussel)<br />[📖](https://github.com/snipe/snipe-it/commits?author=FleischKarussel "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/319644?v=3" width="110px;"/><br /><sub>Dylan Yi</sub>](https://github.com/feeva)<br />[💻](https://github.com/snipe/snipe-it/commits?author=feeva "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/857740?v=3" width="110px;"/><br /><sub>Gil Rutkowski</sub>](http://FlashingCursor.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=flashingcursor "Code") | [<img src="https://avatars3.githubusercontent.com/u/129360?v=3" width="110px;"/><br /><sub>Desmond Morris</sub>](http://www.desmondmorris.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=desmondmorris "Code") | [<img src="https://avatars2.githubusercontent.com/u/52936?v=3" width="110px;"/><br /><sub>Nick Peelman</sub>](http://peelman.us)<br />[💻](https://github.com/snipe/snipe-it/commits?author=peelman "Code") | [<img src="https://avatars0.githubusercontent.com/u/53161?v=3" width="110px;"/><br /><sub>Abraham Vegh</sub>](https://abrahamvegh.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=abrahamvegh "Code") | [<img src="https://avatars0.githubusercontent.com/u/2818680?v=3" width="110px;"/><br /><sub>Mohamed Rashid</sub>](https://github.com/rashivkp)<br />[📖](https://github.com/snipe/snipe-it/commits?author=rashivkp "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/1509456?v=3" width="110px;"/><br /><sub>Kasey</sub>](http://hinchk.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=HinchK "Code") | [<img src="https://avatars2.githubusercontent.com/u/10522541?v=3" width="110px;"/><br /><sub>Brett</sub>](https://github.com/BrettFagerlund)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=BrettFagerlund "Tests") |
| [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
| [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
| [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/4930051?v=4" width="110px;"/><br /><sub>Wes Hulette</sub>](http://macfoo.wordpress.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [<img src="https://avatars0.githubusercontent.com/u/8134591?v=4" width="110px;"/><br /><sub>patrict</sub>](https://github.com/patrict)<br />[💻](https://github.com/snipe/snipe-it/commits?author=patrict "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/2611616?v=4" width="110px;"/><br /><sub>Dmitriy Minaev</sub>](https://github.com/VELIKII-DIVAN)<br />[💻](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [<img src="https://avatars0.githubusercontent.com/u/5132245?v=4" width="110px;"/><br /><sub>liquidhorse</sub>](https://github.com/liquidhorse)<br />[💻](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [<img src="https://avatars1.githubusercontent.com/u/183678?v=4" width="110px;"/><br /><sub>Jordi Boggiano</sub>](https://seld.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [<img src="https://avatars0.githubusercontent.com/u/653557?v=4" width="110px;"/><br /><sub>Ivan Nieto</sub>](https://github.com/inietov)<br />[💻](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [<img src="https://avatars2.githubusercontent.com/u/6764151?v=4" width="110px;"/><br /><sub>Ben RUBSON</sub>](https://github.com/benrubson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [<img src="https://avatars2.githubusercontent.com/u/8554558?v=4" width="110px;"/><br /><sub>NMathar</sub>](https://github.com/NMathar)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [<img src="https://avatars1.githubusercontent.com/u/139566?v=4" width="110px;"/><br /><sub>Steffen</sub>](https://github.com/smb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smb "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/6609453?v=4" width="110px;"/><br /><sub>Sxderp</sub>](https://github.com/Sxderp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [<img src="https://avatars1.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>fanta8897</sub>](https://github.com/fanta8897)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [<img src="https://avatars2.githubusercontent.com/u/2576509?v=4" width="110px;"/><br /><sub>Andrey Bolonin</sub>](https://andreybolonin.com/phpconsulting/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [<img src="https://avatars3.githubusercontent.com/u/2173307?v=4" width="110px;"/><br /><sub>shinayoshi</sub>](http://www.shinayoshi.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [<img src="https://avatars3.githubusercontent.com/u/2130159?v=4" width="110px;"/><br /><sub>Hubert</sub>](https://github.com/reuser)<br />[💻](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [<img src="https://avatars0.githubusercontent.com/u/6865789?v=4" width="110px;"/><br /><sub>KeenRivals</sub>](https://brashear.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [<img src="https://avatars3.githubusercontent.com/u/2902513?v=4" width="110px;"/><br /><sub>omyno</sub>](https://github.com/omyno)<br />[💻](https://github.com/snipe/snipe-it/commits?author=omyno "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/6271335?v=4" width="110px;"/><br /><sub>Evgeny</sub>](https://github.com/jackka)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jackka "Code") | [<img src="https://avatars2.githubusercontent.com/u/1169963?v=4" width="110px;"/><br /><sub>Colin Campbell</sub>](https://digitalist.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=colin-campbell "Code") | [<img src="https://avatars3.githubusercontent.com/u/2872098?v=4" width="110px;"/><br /><sub>Ľubomír Kučera</sub>](https://github.com/lubo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lubo "Code") | [<img src="https://avatars3.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://www.sourceguru.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mezzle "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [<img src="https://avatars0.githubusercontent.com/u/17459600?v=4" width="110px;"/><br /><sub>Marián Skrip</sub>](https://github.com/mskrip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | [<img src="https://avatars2.githubusercontent.com/u/47435081?v=4" width="110px;"/><br /><sub>Godfrey Martinez</sub>](https://github.com/Godmartinz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/2075128?v=4" width="110px;"/><br /><sub>bigtreeEdo</sub>](https://github.com/bigtreeEdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [<img src="https://avatars0.githubusercontent.com/u/5000430?v=4" width="110px;"/><br /><sub>Colin McNeil</sub>](https://colinmcneil.me/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [<img src="https://avatars0.githubusercontent.com/u/421625?v=4" width="110px;"/><br /><sub>JoKneeMo</sub>](https://github.com/JoKneeMo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [<img src="https://avatars0.githubusercontent.com/u/54849013?v=4" width="110px;"/><br /><sub>Joshi</sub>](http://www.redbridge.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [<img src="https://avatars2.githubusercontent.com/u/15731458?v=4" width="110px;"/><br /><sub>Anthony Burns</sub>](https://github.com/anthonypburns)<br />[💻](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [<img src="https://avatars1.githubusercontent.com/u/63399474?v=4" width="110px;"/><br /><sub>johnson-yi</sub>](https://github.com/johnson-yi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=johnson-yi "Code") | [<img src="https://avatars1.githubusercontent.com/u/1862720?v=4" width="110px;"/><br /><sub>Sanjay Govind</sub>](https://tangentmc.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sanjay900 "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/1255375?v=4" width="110px;"/><br /><sub>Peter Upfold</sub>](https://peter.upfold.org.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterUpfold "Code") | [<img src="https://avatars2.githubusercontent.com/u/961717?v=4" width="110px;"/><br /><sub>Jared Biel</sub>](https://github.com/jbiel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbiel "Code") | [<img src="https://avatars1.githubusercontent.com/u/1733625?v=4" width="110px;"/><br /><sub>Dampfklon</sub>](https://github.com/dampfklon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dampfklon "Code") | [<img src="https://avatars2.githubusercontent.com/u/52973156?v=4" width="110px;"/><br /><sub>Charles Hamilton</sub>](https://communityclosing.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chamilton-ccn "Code") | [<img src="https://avatars.githubusercontent.com/u/551789?v=4" width="110px;"/><br /><sub>Giuseppe Iannello</sub>](https://github.com/giannello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=giannello "Code") | [<img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="110px;"/><br /><sub>Peter Dave Hello</sub>](https://www.peterdavehello.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterDaveHello "Code") | [<img src="https://avatars.githubusercontent.com/u/6106332?v=4" width="110px;"/><br /><sub>sigmoidal</sub>](https://github.com/sigmoidal)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sigmoidal "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2082554?v=4" width="110px;"/><br /><sub>Vincent Lainé</sub>](https://github.com/phenixdotnet)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phenixdotnet "Code") | [<img src="https://avatars.githubusercontent.com/u/1943040?v=4" width="110px;"/><br /><sub>Lucas Pleß</sub>](http://www.lucas-pless.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derlucas "Code") | [<img src="https://avatars.githubusercontent.com/u/472804?v=4" width="110px;"/><br /><sub>Ian Littman</sub>](http://twitter.com/iansltx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=iansltx "Code") | [<img src="https://avatars.githubusercontent.com/u/3519029?v=4" width="110px;"/><br /><sub>João Paulo</sub>](https://github.com/PauloLuna)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PauloLuna "Code") | [<img src="https://avatars.githubusercontent.com/u/70443365?v=4" width="110px;"/><br /><sub>ThoBur</sub>](https://github.com/ThoBur)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ThoBur "Code") | [<img src="https://avatars.githubusercontent.com/u/1972329?v=4" width="110px;"/><br /><sub>Alexander Chibrikin</sub>](http://phpprofi.ru/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | [<img src="https://avatars.githubusercontent.com/u/438332?v=4" width="110px;"/><br /><sub>Anthony Winstanley</sub>](https://github.com/winstan)<br />[💻](https://github.com/snipe/snipe-it/commits?author=winstan "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3075214?v=4" width="110px;"/><br /><sub>Folke</sub>](https://github.com/fashberg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fashberg "Code") | [<img src="https://avatars.githubusercontent.com/u/1351571?v=4" width="110px;"/><br /><sub>Bennett Blodinger</sub>](https://github.com/benwa)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benwa "Code") | [<img src="https://avatars.githubusercontent.com/u/2974631?v=4" width="110px;"/><br /><sub>NMC</sub>](https://nmc.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ncareau "Code") | [<img src="https://avatars.githubusercontent.com/u/52182449?v=4" width="110px;"/><br /><sub>andres-baller</sub>](https://github.com/andres-baller)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andres-baller "Code") | [<img src="https://avatars.githubusercontent.com/u/67109348?v=4" width="110px;"/><br /><sub>sean-borg</sub>](https://github.com/sean-borg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sean-borg "Code") | [<img src="https://avatars.githubusercontent.com/u/32170051?v=4" width="110px;"/><br /><sub>EDVLeer</sub>](https://github.com/EDVLeer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EDVLeer "Code") | [<img src="https://avatars.githubusercontent.com/u/23075196?v=4" width="110px;"/><br /><sub>Kurokat</sub>](https://github.com/Kurokat)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Kurokat "Code") |
| [<img src="https://avatars.githubusercontent.com/u/915514?v=4" width="110px;"/><br /><sub>Kevin Köllmann</sub>](https://www.kevinkoellmann.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koelle25 "Code") | [<img src="https://avatars.githubusercontent.com/u/49025941?v=4" width="110px;"/><br /><sub>sw-mreyes</sub>](https://github.com/sw-mreyes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sw-mreyes "Code") | [<img src="https://avatars.githubusercontent.com/u/70129?v=4" width="110px;"/><br /><sub>Joel Pittet</sub>](https://pittet.ca)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joelpittet "Code") | [<img src="https://avatars.githubusercontent.com/u/792695?v=4" width="110px;"/><br /><sub>Eli Young</sub>](https://elyscape.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=elyscape "Code") | [<img src="https://avatars.githubusercontent.com/u/317015?v=4" width="110px;"/><br /><sub>Raell Dottin</sub>](https://github.com/raelldottin)<br />[💻](https://github.com/snipe/snipe-it/commits?author=raelldottin "Code") | [<img src="https://avatars.githubusercontent.com/u/1446856?v=4" width="110px;"/><br /><sub>Tom Misilo</sub>](https://github.com/misilot)<br />[💻](https://github.com/snipe/snipe-it/commits?author=misilot "Code") | [<img src="https://avatars.githubusercontent.com/u/4496300?v=4" width="110px;"/><br /><sub>David Davenne</sub>](http://david.davenne.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JuustoMestari "Code") |
| [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
| [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
| [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/qveensi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
| [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
| [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
| [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
| [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
| [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3302372?v=4" width="110px;"/><br /><sub>Jared Busch</sub>](https://github.com/sorvani)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [<img src="https://avatars.githubusercontent.com/u/111956991?v=4" width="110px;"/><br /><sub>seanborg-codethink</sub>](https://github.com/seanborg-codethink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [<img src="https://avatars.githubusercontent.com/u/160669961?v=4" width="110px;"/><br /><sub>dkaatz</sub>](https://github.com/dkaatz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [<img src="https://avatars.githubusercontent.com/u/827205?v=4" width="110px;"/><br /><sub>Daniel Ruf</sub>](https://threema.id/74SF7MW6?text=)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [<img src="https://avatars.githubusercontent.com/u/38883201?v=4" width="110px;"/><br /><sub>ahpaleus</sub>](https://github.com/ahpaleus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [<img src="https://avatars.githubusercontent.com/u/22906055?v=4" width="110px;"/><br /><sub>Anh DAO-DUY</sub>](https://github.com/mink-adao-duy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [<img src="https://avatars.githubusercontent.com/u/4723453?v=4" width="110px;"/><br /><sub>Andres Gutierrez</sub>](https://github.com/Serdnad)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111083379?v=4" width="110px;"/><br /><sub>Warren White</sub>](https://github.com/wewhite)<br />[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [<img src="https://avatars.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://robintemme.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [<img src="https://avatars.githubusercontent.com/u/47008367?v=4" width="110px;"/><br /><sub>herroworrd</sub>](https://github.com/herroworrd)<br />[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [<img src="https://avatars.githubusercontent.com/u/28558609?v=4" width="110px;"/><br /><sub>vicleos</sub>](https://mubiu.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [<img src="https://avatars.githubusercontent.com/u/1016780?v=4" width="110px;"/><br /><sub>Bob Clough</sub>](http://thinkl33t.co.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [<img src="https://avatars.githubusercontent.com/u/10648463?v=4" width="110px;"/><br /><sub>Brandon Daniel Bailey</sub>](https://github.com/brandon-bailey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [<img src="https://avatars.githubusercontent.com/u/23556080?v=4" width="110px;"/><br /><sub>Marc Bartelt</sub>](https://github.com/marcquark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
| [<img src="https://avatars.githubusercontent.com/u/18286893?v=4" width="110px;"/><br /><sub>manu-crealytics</sub>](https://github.com/manu-crealytics)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [<img src="https://avatars.githubusercontent.com/u/685167?v=4" width="110px;"/><br /><sub>Deloz</sub>](https://deloz.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [<img src="https://avatars.githubusercontent.com/u/2682426?v=4" width="110px;"/><br /><sub>Martin Berg</sub>](https://github.com/mbrrg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [<img src="https://avatars.githubusercontent.com/u/3694534?v=4" width="110px;"/><br /><sub>Richard Schwab</sub>](https://github.com/Nothing4You)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [<img src="https://avatars.githubusercontent.com/u/8959676?v=4" width="110px;"/><br /><sub>Rick Heil</sub>](https://rickheil.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [<img src="https://avatars.githubusercontent.com/u/397106?v=4" width="110px;"/><br /><sub>Ross Crawford-d'Heureuse</sub>](https://github.com/rosscdh)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1621107?v=4" width="110px;"/><br /><sub>Ryan McGuire</sub>](https://github.com/McG800)<br />[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [<img src="https://avatars.githubusercontent.com/u/77835667?v=4" width="110px;"/><br /><sub>SBrown2021</sub>](https://github.com/SBrown2021)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [<img src="https://avatars.githubusercontent.com/u/8780913?v=4" width="110px;"/><br /><sub>Serkan</sub>](https://github.com/serkanerip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [<img src="https://avatars.githubusercontent.com/u/63188620?v=4" width="110px;"/><br /><sub>Shanks</sub>](https://www.yudelei.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [<img src="https://avatars.githubusercontent.com/u/198525698?v=4" width="110px;"/><br /><sub>cendai-mis</sub>](https://github.com/cendai-mis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [<img src="https://avatars.githubusercontent.com/u/8724583?v=4" width="110px;"/><br /><sub>Shaun McPeck</sub>](https://smcpeck.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [<img src="https://avatars.githubusercontent.com/u/1378836?v=4" width="110px;"/><br /><sub>Stephen</sub>](https://github.com/snazy2000)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/4462739?v=4" width="110px;"/><br /><sub>Steven</sub>](http://nevets82.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [<img src="https://avatars.githubusercontent.com/u/29017267?v=4" width="110px;"/><br /><sub>Mateus Villar</sub>](https://mateusvillar.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [<img src="https://avatars.githubusercontent.com/u/12749393?v=4" width="110px;"/><br /><sub>Matthew Zackschewski</sub>](https://github.com/mzack5020)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [<img src="https://avatars.githubusercontent.com/u/12660103?v=4" width="110px;"/><br /><sub>Matthias Frei</sub>](https://www.frei.media/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [<img src="https://avatars.githubusercontent.com/u/824840?v=4" width="110px;"/><br /><sub>Nenad Ticaric</sub>](https://github.com/nticaric)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [<img src="https://avatars.githubusercontent.com/u/706439?v=4" width="110px;"/><br /><sub>Nikolay Didenko</sub>](https://github.com/Scorcher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [<img src="https://avatars.githubusercontent.com/u/5457236?v=4" width="110px;"/><br /><sub>Nuno Maduro</sub>](https://nunomaduro.com/sponsorships)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
| [<img src="https://avatars.githubusercontent.com/u/8883074?v=4" width="110px;"/><br /><sub>Oliver Walerys</sub>](https://tektikhq.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [<img src="https://avatars.githubusercontent.com/u/3102039?v=4" width="110px;"/><br /><sub>R. Christian McDonald</sub>](https://keybase.io/rcmcdonald91)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [<img src="https://avatars.githubusercontent.com/u/1525581?v=4" width="110px;"/><br /><sub>nix</sub>](https://nnix.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [<img src="https://avatars.githubusercontent.com/u/55462380?v=4" width="110px;"/><br /><sub>octobunny</sub>](https://github.com/octobunny)<br />[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [<img src="https://avatars.githubusercontent.com/u/8558670?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/sreyemnayr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>p3nj</sub>](https://benji.ltd/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [<img src="https://avatars.githubusercontent.com/u/6201617?v=4" width="110px;"/><br /><sub>Tim White</sub>](https://github.com/timwsuqld)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
| [<img src="https://avatars.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>yannikp</sub>](https://github.com/yannikp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [<img src="https://avatars.githubusercontent.com/u/20525448?v=4" width="110px;"/><br /><sub>victoria</sub>](https://github.com/viclou)<br />[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [<img src="https://avatars.githubusercontent.com/u/40685314?v=4" width="110px;"/><br /><sub>Valentyn Tulub</sub>](https://github.com/valentyntu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [<img src="https://avatars.githubusercontent.com/u/864520?v=4" width="110px;"/><br /><sub>Wouter van Os</sub>](http://wouter0100.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [<img src="https://avatars.githubusercontent.com/u/3946540?v=4" width="110px;"/><br /><sub>Wyatt Teeter</sub>](https://www.linkedin.com/in/wyatt-teeter)<br />[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [<img src="https://avatars.githubusercontent.com/u/1596124?v=4" width="110px;"/><br /><sub>Yorick Terweijden</sub>](https://github.com/terwey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [<img src="https://avatars.githubusercontent.com/u/69298836?v=4" width="110px;"/><br /><sub>bmkalle</sub>](https://github.com/bmkalle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28403467?v=4" width="110px;"/><br /><sub>bricelabelle</sub>](https://github.com/bricelabelle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [<img src="https://avatars.githubusercontent.com/u/97770090?v=4" width="110px;"/><br /><sub>corydlamb</sub>](https://github.com/corydlamb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [<img src="https://avatars.githubusercontent.com/u/1154133?v=4" width="110px;"/><br /><sub>Diogenes S. Jesus</sub>](http://twitter.com/splash)<br />[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [<img src="https://avatars.githubusercontent.com/u/5826629?v=4" width="110px;"/><br /><sub>D M</sub>](https://github.com/dkmansion)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [<img src="https://avatars.githubusercontent.com/u/14837699?v=4" width="110px;"/><br /><sub>Dustin B</sub>](https://github.com/Jarli01)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [<img src="https://avatars.githubusercontent.com/u/348344?v=4" width="110px;"/><br /><sub>Fabian Grutschus</sub>](https://github.com/fabiang)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [<img src="https://avatars.githubusercontent.com/u/1491053?v=4" width="110px;"/><br /><sub>MelonSmasher</sub>](https://github.com/MelonSmasher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
| [<img src="https://avatars.githubusercontent.com/u/80526133?v=4" width="110px;"/><br /><sub>AlexanderWPapyrus</sub>](https://github.com/AlexanderWPapyrus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [<img src="https://avatars.githubusercontent.com/u/306231?v=4" width="110px;"/><br /><sub>Alexandr Hacicheant</sub>](https://github.com/disc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [<img src="https://avatars.githubusercontent.com/u/3032891?v=4" width="110px;"/><br /><sub>Hex</sub>](https://hex128.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [<img src="https://avatars.githubusercontent.com/u/8697942?v=4" width="110px;"/><br /><sub>Arunas Skirius</sub>](https://github.com/arukompas)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [<img src="https://avatars.githubusercontent.com/u/104396?v=4" width="110px;"/><br /><sub>Ben Periton</sub>](https://github.com/benperiton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [<img src="https://avatars.githubusercontent.com/u/11906832?v=4" width="110px;"/><br /><sub>Byron Wolfman</sub>](https://wolfman.dev/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [<img src="https://avatars.githubusercontent.com/u/56485508?v=4" width="110px;"/><br /><sub>Calvin</sub>](https://github.com/CalvinSchwartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
| [<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") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,6 +1,6 @@
![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class FixUpAssignedTypeWithoutAssignedTo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-type-fixup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_type but no assigned_to';
/**
* Execute the console command.
*/
public function handle()
{
DB::table('assets')->whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
$this->info("Assets with an assigned_type but no assigned_to are fixed");
}
}

View File

@@ -182,7 +182,7 @@ class LdapSync extends Command
// Inject location information fields
for ($i = 0; $i < $results['count']; $i++) {
$results[$i]['ldap_location_override'] = false;
$results[$i]['location_id'] = 0;
$results[$i]['location_id'] = null;
}
// Grab subsets based on location-specific DNs, and overwrite location for these users.

View File

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Schema;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
@@ -66,8 +66,8 @@ class PaveIt extends Command
foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
if (\Schema::hasColumn('assets', $custom_field->db_column)) {
\Schema::table('assets', function ($table) use ($custom_field) {
if (Schema::hasColumn('assets', $custom_field->db_column)) {
Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
}
@@ -84,8 +84,8 @@ class PaveIt extends Command
}
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
\DB::statement('delete from oauth_clients WHERE id > 2');
\DB::statement('delete from oauth_access_tokens WHERE id > 2');
DB::statement('delete from oauth_clients WHERE id > 2');
DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
}
}

View File

@@ -249,6 +249,7 @@ class RestoreFromBackup extends Command
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/locations',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
@@ -328,9 +329,9 @@ class RestoreFromBackup extends Command
}
}
}
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', 'avif'
];
$good_extensions = config('filesystems.allowed_upload_extensions_array');
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {

View File

@@ -99,8 +99,11 @@ class SendAcceptanceReminder extends Command
foreach ($no_email_list as $user) {
$rows[] = [$user['id'], $user['name']];
}
$this->info("The following users do not have an email address:");
$this->table($headers, $rows);
if (!empty($rows)) {
$this->info("The following users do not have an email address:");
$this->table($headers, $rows);
}
return 0;
}

View File

@@ -19,7 +19,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
if(Setting::getSettings()->alerts_enabled === 1) {
if(Setting::getSettings()?->alerts_enabled === 1) {
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();

View File

@@ -28,7 +28,7 @@ class CheckoutableCheckedIn
$this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy;
$this->note = $note;
$this->action_date = $action_date ?? date('Y-m-d');
$this->action_date = $action_date ?? date('Y-m-d H:i:s');
$this->originalValues = $originalValues;
}
}

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
@@ -107,18 +108,25 @@ class Handler extends ExceptionHandler
$statusCode = $e->getStatusCode();
// API throttle requests are handled in the RouteServiceProvider configureRateLimiting() method, so we don't need to handle them here
switch ($e->getStatusCode()) {
case '404':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
case '429':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
}
}
// This handles API validation exceptions that happen at the Form Request level, so they
// never even get to the controller where we normally nicely format JSON responses
if ($e instanceof ValidationException) {
$response = $this->invalidJson($request, $e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
}
@@ -143,6 +151,10 @@ 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') {
$route = 'fields.index';
}
return redirect()
@@ -201,6 +213,7 @@ class Handler extends ExceptionHandler
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});

View File

@@ -13,6 +13,7 @@ use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\License;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Carbon\Carbon;
@@ -722,8 +723,8 @@ class Helper
// The check and message that the user is still using the deprecated version
$deprecations = [
'ms_teams_deprecated' => array(
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
];
// if item of concern is being used and its being used with the deprecated values return the notification array.
@@ -876,6 +877,48 @@ class Helper
return false;
}
/**
* Check if the file is a video, so we can show a preview
*
* @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)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'audio/mpeg') || ($filetype == 'audio/ogg')) {
return $filetype;
}
return false;
}
/**
* Walks through the permissions in the permissions config file and determines if
* permissions are granted based on a $selected_arr array.
@@ -1170,6 +1213,15 @@ class Helper
// Misc
'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save',
// video
'mov' => 'fa-solid fa-video',
'mp4' => 'fa-solid fa-video',
// audio
'ogg' => 'fa-solid fa-file-audio',
'mp3' => 'fa-solid fa-file-audio',
'wav' => 'fa-solid fa-file-audio',
];
if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
@@ -1313,25 +1365,24 @@ class Helper
switch ($item) {
case 'asset':
return 'fas fa-barcode';
break;
case 'accessory':
return 'fas fa-keyboard';
break;
case 'component':
return 'fas fa-hdd';
break;
case 'consumable':
return 'fas fa-tint';
break;
case 'license':
return 'far fa-save';
break;
case 'location':
return 'fas fa-map-marker-alt';
break;
case 'user':
return 'fas fa-user';
break;
case 'supplier':
return 'fa-solid fa-store';
case 'manufacturer':
return 'fa-solid fa-building';
case 'category':
return 'fa-solid fa-table-columns';
}
}
@@ -1481,60 +1532,67 @@ class Helper
}
static public function getRedirectOption($request, $id, $table, $item_id = null)
static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
{
$redirect_option = Session::get('redirect_option');
$checkout_to_type = Session::get('checkout_to_type');
$redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
$checkout_to_type = Session::get('checkout_to_type') ?? null;
$checkedInFrom = Session::get('checkedInFrom');
$other_redirect = Session::get('other_redirect');
$backUrl = Session::pull('back_url', route('home'));
// 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);
}
// return to index
if ($redirect_option == 'index') {
switch ($table) {
case "Assets":
return route('hardware.index');
case "Users":
return route('users.index');
case "Licenses":
return route('licenses.index');
case "Accessories":
return route('accessories.index');
case "Components":
return route('components.index');
case "Consumables":
return route('consumables.index');
}
return match ($table) {
'Assets' => redirect()->route('hardware.index'),
'Users' => redirect()->route('users.index'),
'Licenses' => redirect()->route('licenses.index'),
'Accessories' => redirect()->route('accessories.index'),
'Components' => redirect()->route('components.index'),
'Consumables' => redirect()->route('consumables.index'),
};
}
// return to thing being assigned
if ($redirect_option == 'item') {
switch ($table) {
case "Assets":
return route('hardware.show', $id ?? $item_id);
case "Users":
return route('users.show', $id ?? $item_id);
case "Licenses":
return route('licenses.show', $id ?? $item_id);
case "Accessories":
return route('accessories.show', $id ?? $item_id);
case "Components":
return route('components.show', $id ?? $item_id);
case "Consumables":
return route('consumables.show', $id ?? $item_id);
}
return match ($table) {
'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
'Users' => redirect()->route('users.show', $id ?? $item_id),
'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
'Components' => redirect()->route('components.show', $id ?? $item_id),
'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
};
}
// return to assignment target
if ($redirect_option == 'target') {
switch ($checkout_to_type) {
case 'user':
return route('users.show', $request->assigned_user ?? $checkedInFrom);
case 'location':
return route('locations.show', $request->assigned_location ?? $checkedInFrom);
case 'asset':
return route('hardware.show', $request->assigned_asset ?? $checkedInFrom);
}
return match ($checkout_to_type) {
'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
};
}
// return to somewhere else
if ($redirect_option == 'other_redirect') {
return match ($other_redirect) {
'audit' => redirect()->route('assets.audit.due'),
'model' => redirect()->route('models.show', $request->model_id),
};
}
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
}
@@ -1562,6 +1620,11 @@ class Helper
$locations = Location::all();
}
// Bail out early if there are no locations
if ($locations->count() == 0) {
return [];
}
foreach($locations as $location) {
// in case of an update of a single location, use the newly requested company_id
if ($new_company_id) {
@@ -1600,14 +1663,17 @@ class Helper
$items = collect([])->push($location->$keyword);
}
$count = 0;
foreach ($items as $item) {
if ($item && $item->company_id != $location_company) {
$mismatched[] = [
class_basename(get_class($item)),
$item->id,
$item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
str_replace('App\\Models\\', '', $item->assigned_type) ?? null,
$item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
$item->company_id ?? null,
$item->company->name ?? null,
// $item->defaultLoc->id ?? null,
@@ -1619,6 +1685,15 @@ class Helper
$location_company ?? null,
];
$count++;
// Bail early if this is not being run via artisan
if ((!$artisan) && ($count > 0)) {
return $mismatched;
}
}
}
}

View File

@@ -16,14 +16,14 @@ class StorageHelper
$disk = config('filesystems.default');
}
switch (config("filesystems.disks.$disk.driver")) {
case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
default:
return Storage::disk($disk)->download($filename);
default:
return Storage::disk($disk)->download($filename);
}
}
@@ -33,21 +33,29 @@ class StorageHelper
* to determine that they are safe to display inline.
*
* @author <A. Gianotto> [<snipe@snipe.net]>
* @since v7.0.14
* @param $file_with_path
* @since v7.0.14
* @param $file_with_path
* @return bool
*/
public static function allowSafeInline($file_with_path) {
public static function allowSafeInline($file_with_path)
{
$allowed_inline = [
'pdf',
'svg',
'jpg',
'gif',
'svg',
'avif',
'webp',
'gif',
'gif',
'jpg',
'mov',
'mp3',
'mp4',
'ogg',
'pdf',
'png',
'svg',
'svg',
'wav',
'webm',
'webp',
];
@@ -59,10 +67,24 @@ class StorageHelper
}
public static function getFiletype($file_with_path)
{
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
return pathinfo($file_with_path, PATHINFO_EXTENSION);
}
return null;
}
/**
* Decide whether to show the file inline or download it.
*/
public static function showOrDownloadFile($file, $filename) {
public static function showOrDownloadFile($file, $filename)
{
$headers = [];

View File

@@ -83,7 +83,8 @@ class AccessoriesController extends Controller
// Was the accessory created?
if ($accessory->save()) {
// Redirect to the new accessory page
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success'));
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
@@ -167,7 +168,8 @@ class AccessoriesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($accessory->save()) {
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success'));
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.update.success'));
}
} else {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
@@ -220,7 +222,10 @@ class AccessoriesController extends Controller
*/
public function show(Accessory $accessory) : View | RedirectResponse
{
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id);
$accessory->loadCount('checkouts as checkouts_count');
$accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
$this->authorize('view', $accessory);
return view('accessories.view', compact('accessory'));
}

View File

@@ -78,7 +78,8 @@ class AccessoryCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success'));
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkin.success'));
}
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));

View File

@@ -97,7 +97,7 @@ class AccessoryCheckoutController extends Controller
// Redirect to the new accessory page
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkout.success'));
}
}

View File

@@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
@@ -20,9 +21,12 @@ use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
@@ -148,6 +152,8 @@ class AcceptanceController extends Controller
}
}
$assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
@@ -157,35 +163,30 @@ class AcceptanceController extends Controller
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
$display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id);
$display_model = $license->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
@@ -226,7 +227,7 @@ class AcceptanceController extends Controller
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_to,
'assigned_to' => $assigned_user->present()->fullName,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
@@ -240,6 +241,19 @@ class AcceptanceController extends Controller
}
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
// Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename;
try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data));
} catch (\Exception $e) {
Log::warning($e);
}
}
try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
} catch (\Exception $e) {
@@ -337,6 +351,21 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.declined');
}
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
$recipient,
$request->input('asset_acceptance') === 'accepted',
));
}
} catch (Exception $e) {
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);

View File

@@ -1,200 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use App\Http\Transformers\UploadedFilesTransformer;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Http\Request;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list(Asset $asset, Request $request) : JsonResponse | array
{
$this->authorize('view', $asset);
$allowed_columns =
[
'id',
'filename',
'eol',
'notes',
'created_at',
'updated_at',
];
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
if ($request->filled('search')) {
$files = $files->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $files->count()) ? $files->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') : 'created_at';
$files = $files->orderBy($sort, $order);
$files = $files->skip($offset)->take($limit)->get();
return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy(Asset $asset, $fileId = null) : JsonResponse
{
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}

View File

@@ -75,6 +75,7 @@ class AssetMaintenancesController extends Controller
'serial',
'created_by',
'supplier',
'location',
'is_warranty',
'status_label',
];
@@ -98,6 +99,9 @@ class AssetMaintenancesController extends Controller
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'location':
$maintenances = $maintenances->OrderLocationName($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;

View File

@@ -1,184 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\AssetModelsTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetmodel
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetmodel_id) : JsonResponse | array
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetmodel_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
$this->authorize('view', $assetmodel);
return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View File

@@ -85,6 +85,12 @@ class AssetModelsController extends Controller
$assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
}
if ($request->input('requestable') == 'true') {
$assetmodels = $assetmodels->where('models.requestable', '=', '1');
} elseif ($request->input('requestable') == 'false') {
$assetmodels = $assetmodels->where('models.requestable', '=', '0');
}
if ($request->filled('notes')) {
$assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
}
@@ -148,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()));
@@ -201,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.
@@ -216,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

@@ -114,6 +114,7 @@ class AssetsController extends Controller
'byod',
'asset_eol_date',
'requestable',
'jobtitle',
];
$filter = [];
@@ -395,6 +396,9 @@ class AssetsController extends Controller
case 'assigned_to':
$assets->OrderAssigned($order);
break;
case 'jobtitle':
$assets->OrderByJobTitle($order);
break;
case 'created_by':
$assets->OrderByCreatedByName($order);
break;
@@ -701,7 +705,9 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
// below is what we want the _eventual_ return to look like - in a more standardized format.
// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@@ -1139,12 +1145,12 @@ class AssetsController extends Controller
}
}
// Validate custom fields
Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate();
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
}

View File

@@ -56,7 +56,7 @@ class CategoriesController extends Controller
'notes',
])
->with('adminuser')
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
/*
@@ -212,7 +212,7 @@ class CategoriesController extends Controller
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
if (! $category->isDeletable()) {
return response()->json(

View File

@@ -43,7 +43,10 @@ class CompaniesController extends Controller
$companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow();
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
}])
->with('adminuser')
->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
if ($request->filled('search')) {
$companies->TextSearch($request->input('search'));
@@ -119,6 +122,7 @@ class CompaniesController extends Controller
{
$this->authorize('view', Company::class);
$company = Company::findOrFail($id);
$this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company);
}
@@ -136,6 +140,7 @@ class CompaniesController extends Controller
{
$this->authorize('update', Company::class);
$company = Company::findOrFail($id);
$this->authorize('update', $company);
$company->fill($request->all());
$company = $request->handleImages($company);
@@ -188,6 +193,7 @@ class CompaniesController extends Controller
'companies.image',
]);
if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
}

View File

@@ -234,6 +234,15 @@ class ImportController extends Controller
case 'location':
$redirectTo = 'locations.index';
break;
case 'supplier':
$redirectTo = 'suppliers.index';
break;
case 'manufacturer':
$redirectTo = 'manufacturers.index';
break;
case 'category':
$redirectTo = 'categories.index';
break;
}
if ($errors) { //Failure

View File

@@ -29,12 +29,21 @@ class LicenseSeatsController extends Controller
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId);
if ($request->input('status') == 'available') {
$seats->whereNull('license_seats.assigned_to');
}
if ($request->input('status') == 'assigned') {
$seats->ByAssigned();
}
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort') == 'department') {
$seats->OrderDepartments($order);
} else {
$seats->orderBy('id', $order);
$seats->orderBy('updated_at', $order);
}
$total = $seats->count();

View File

@@ -87,7 +87,8 @@ class LocationsController extends Controller
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count');
->withCount('users as users_count')
->with('adminuser');
// Only scope locations if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) {
@@ -218,6 +219,7 @@ class LocationsController extends Controller
'locations.updated_at',
'locations.image',
'locations.currency',
'locations.company_id',
'locations.notes',
])
->withCount('assignedAssets as assigned_assets_count')

View File

@@ -4,15 +4,19 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ProfileTransformer;
use App\Models\CheckoutRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\Gate;
use App\Models\CustomField;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ProfileController extends Controller
{
@@ -167,6 +171,22 @@ class ProfileController extends Controller
}
/**
* Display the EULAs accepted by the user.
*
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(ProfileTransformer $transformer)
{
// Only return this user's EULAs
$eulas = auth()->user()->eulas;
return response()->json(
$transformer->transformFiles($eulas, $eulas->count())
);
}
}

View File

@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ActionlogsTransformer;
use App\Models\Actionlog;
use App\Models\Company;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -18,10 +20,11 @@ class ReportsController extends Controller
*/
public function index(Request $request) : JsonResponse | array
{
$this->authorize('reports.view');
$this->authorize('activity.view');
$actionlogs = Actionlog::with('item', 'user', 'adminuser', 'target', 'location');
if ($request->filled('search')) {
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
}

View File

@@ -24,10 +24,15 @@ class SuppliersController extends Controller
public function index(Request $request): array
{
$this->authorize('view', Supplier::class);
$allowed_columns = ['
id',
$allowed_columns = [
'id',
'name',
'address',
'address2',
'city',
'state',
'country',
'zip',
'phone',
'contact',
'fax',
@@ -39,21 +44,24 @@ class SuppliersController extends Controller
'components_count',
'consumables_count',
'url',
'notes',
];
$suppliers = Supplier::select(
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url'])
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'created_by', 'updated_at', 'deleted_at', 'image', 'notes', 'url', 'zip'])
->withCount('assets as assets_count')
->withCount('licenses as licenses_count')
->withCount('accessories as accessories_count')
->withCount('components as components_count')
->withCount('consumables as consumables_count');
->withCount('consumables as consumables_count')
->with('adminuser');
if ($request->filled('search')) {
$suppliers = $suppliers->TextSearch($request->input('search'));
$suppliers->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$suppliers->where('name', '=', $request->input('name'));
}
@@ -100,7 +108,15 @@ class SuppliersController extends Controller
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$suppliers->orderBy($sort, $order);
switch ($request->input('sort')) {
case 'created_by':
$suppliers->OrderByCreatedByName($order);
break;
default:
$suppliers->orderBy($sort, $order);
break;
}
$total = $suppliers->count();
$suppliers = $suppliers->skip($offset)->take($limit)->get();

View File

@@ -0,0 +1,258 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
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;
class UploadedFilesController extends Controller
{
static $map_object_type = [
'accessories' => Accessory::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/',
'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/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'hardware' => 'asset',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/**
* List files for an object
*
* @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 list files for
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function index(Request $request, $object_type, $id) : JsonResponse | array
{
// 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 response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Columns allowed for sorting
$allowed_columns =
[
'id',
'filename',
'action_type',
'note',
'created_at',
];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->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';
// 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
if ($request->filled('search')) {
$uploads->where(
function ($query) use ($request) {
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
}
);
}
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
}
/**
* 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.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
{
// 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 response()->json(Helper::formatStandardApiResponse('error', null, 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 response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
}
// No files were submitted
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
}
/**
* Check for permissions and display the 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.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function show($object_type, $id, $file_id) : JsonResponse | 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 response()->json(Helper::formatStandardApiResponse('error', null, 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 response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200);
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
}
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.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function destroy($object_type, $id, $file_id) : JsonResponse
{
// 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 response()->json(Helper::formatStandardApiResponse('error', null, 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 response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
}
}

View File

@@ -6,6 +6,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveUserRequest;
use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
@@ -80,7 +81,7 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas')
->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
@@ -206,11 +207,11 @@ class UsersController extends Controller
}
if ($request->filled('manages_users_count')) {
$users->has('manages_users_count', '=', $request->input('manages_users_count'));
$users->has('managesUsers', '=', $request->input('manages_users_count'));
}
if ($request->filled('manages_locations_count')) {
$users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
$users->has('managedLocations', '=', $request->input('manages_locations_count'));
}
if ($request->filled('autoassign_licenses')) {
@@ -676,7 +677,6 @@ class UsersController extends Controller
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
@@ -736,6 +736,25 @@ class UsersController extends Controller
return (new UsersTransformer)->transformUser($request->user());
}
/**
* Display the EULAs accepted by the user.
*
* @param \App\Models\User $user
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(User $user, ActionlogsTransformer $transformer)
{
$this->authorize('view', User::class);
$eulas = $user->eulas;
return response()->json(
$transformer->transformActionlogs($eulas, $eulas->count())
);
}
/**
* Restore a soft-deleted user.
*

View File

@@ -19,19 +19,6 @@ use \Illuminate\Http\RedirectResponse;
*/
class AssetMaintenancesController extends Controller
{
/**
* Checks for permissions for this action.
*
* @todo This should be replaced with middleware and/or policies
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
private static function getInsufficientPermissionsRedirect(): RedirectResponse
{
return redirect()->route('maintenances.index')
->with('error', trans('general.insufficient_permissions'));
}
/**
* Returns a view that invokes the ajax tables which actually contains
@@ -67,16 +54,10 @@ class AssetMaintenancesController extends Controller
// We have to set this so that the correct property is set in the select2 ajax dropdown
$asset->asset_id = $asset->id;
}
// Prepare Asset Maintenance Type List
$assetMaintenanceType = [
'' => 'Select an asset maintenance type',
] + AssetMaintenance::getImprovementOptions();
// Mark the selected asset, if it came in
return view('asset_maintenances/edit')
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
->with('asset', $asset)
->with('assetMaintenanceType', $assetMaintenanceType)
->with('item', new AssetMaintenance);
}
@@ -91,43 +72,45 @@ class AssetMaintenancesController extends Controller
public function store(Request $request) : RedirectResponse
{
$this->authorize('update', Asset::class);
// create a new model instance
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find($request->input('asset_id'));
if ((! Company::isCurrentUserHasAccess($asset)) && ($asset != null)) {
return static::getInsufficientPermissionsRedirect();
$assets = Asset::whereIn('id', $request->input('selected_assets'))->get();
// 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');
// 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();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
// Was the asset maintenance created?
if (!$assetMaintenance->save()) {
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
}
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id();
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success'));
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
}
/**
@@ -135,26 +118,19 @@ class AssetMaintenancesController extends Controller
*
* @see AssetMaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @version v1.0
* @since [v1.8]
*/
public function edit(AssetMaintenance $maintenance) : View | RedirectResponse
{
$this->authorize('update', Asset::class);
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Prepare Improvement Type List
$assetMaintenanceType = ['' => 'Select an improvement type'] + AssetMaintenance::getImprovementOptions();
$this->authorize('update', $maintenance->asset);
return view('asset_maintenances/edit')
->with('selectedAsset', null)
->with('assetMaintenanceType', $assetMaintenanceType)
->with('item', $maintenance);
->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
->with('asset_ids', request()->input('asset_ids', []))
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
->with('item', $maintenance);
}
/**
@@ -170,33 +146,21 @@ class AssetMaintenancesController extends Controller
public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse
{
$this->authorize('update', Asset::class);
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
$this->authorize('update', $maintenance->asset);
$maintenance->supplier_id = $request->input('supplier_id');
$maintenance->is_warranty = $request->input('is_warranty');
$maintenance->is_warranty = $request->input('is_warranty', 0);
$maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes');
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Save the asset maintenance data
$maintenance->asset_id = $request->input('asset_id');
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->title = $request->input('title');
$maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date');
if (($maintenance->completion_date == null)
) {
// Todo - put this in a getter/setter?
if (($maintenance->completion_date == null))
{
if (($maintenance->asset_maintenance_time !== 0)
|| (! is_null($maintenance->asset_maintenance_time))
) {
@@ -213,10 +177,7 @@ class AssetMaintenancesController extends Controller
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
// Was the asset maintenance created?
if ($maintenance->save()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.edit.success'));
}
@@ -232,21 +193,12 @@ class AssetMaintenancesController extends Controller
* @version v1.0
* @since [v1.8]
*/
public function destroy($assetMaintenanceId) : RedirectResponse
public function destroy(AssetMaintenance $maintenance) : RedirectResponse
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
$this->authorize('update', $maintenance->asset);
// Delete the asset maintenance
$assetMaintenance->delete();
$maintenance->delete();
// Redirect to the asset_maintenance management page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.delete.success'));
@@ -262,11 +214,6 @@ class AssetMaintenancesController extends Controller
*/
public function show(AssetMaintenance $maintenance) : View | RedirectResponse
{
$this->authorize('view', Asset::class);
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
return view('asset_maintenances/view')->with('assetMaintenance', $maintenance);
}
}

View File

@@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Validator;
class AssetCheckinController extends Controller
{
@@ -40,6 +41,14 @@ class AssetCheckinController extends Controller
if (!$asset->model) {
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
$target_option = match ($asset->assigned_type) {
'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset_previous')]),
'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
@@ -87,7 +96,6 @@ class AssetCheckinController extends Controller
});
$asset->expected_checkin = null;
$asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
$asset->name = $request->get('name');
@@ -114,11 +122,14 @@ class AssetCheckinController extends Controller
$originalValues = $asset->getRawOriginal();
// Handle last checkin date
$checkin_at = date('Y-m-d H:i:s');
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
$originalValues['action_date'] = $checkin_at;
$checkin_at = $request->get('checkin_at');
}
$asset->last_checkin = $checkin_at;
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
@@ -142,7 +153,8 @@ class AssetCheckinController extends Controller
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success'));
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkin.success'));
}
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());

View File

@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Session;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Validator;
class AssetCheckoutController extends Controller
{
@@ -36,6 +37,14 @@ class AssetCheckoutController extends Controller
->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'))
->with('statusLabel_list', Helper::deployableStatusLabelList())
@@ -114,7 +123,7 @@ class AssetCheckoutController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error

View File

@@ -149,7 +149,7 @@ class AssetsController extends Controller
$asset->byod = request('byod', 0);
if (! empty($settings->audit_interval)) {
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
$asset->next_audit_date = Carbon::now()->addMonths((int) $settings->audit_interval)->toDateString();
}
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
@@ -188,14 +188,31 @@ class AssetsController extends Controller
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
if (request('assigned_user')) {
$target = User::find(request('assigned_user'));
$target = null;
$location = null;
if ($userId = request('assigned_user')) {
$target = User::find($userId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user'));
}
$location = $target->location_id;
} elseif (request('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
} elseif ($assetId = request('assigned_asset')) {
$target = Asset::find($assetId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset'));
}
$location = $target->location_id;
} elseif (request('assigned_location')) {
$target = Location::find(request('assigned_location'));
} elseif ($locationId = request('assigned_location')) {
$target = Location::find($locationId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.location'));
}
$location = $target->id;
}
@@ -216,18 +233,18 @@ class AssetsController extends Controller
if ($successes) {
if ($failures) {
//some succeeded, some failed
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) //FIXME - not tested
return Helper::getRedirectOption($request, $asset->id, 'Assets') //FIXME - not tested
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]))
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)]));
} else {
if (count($successes) == 1) {
//the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
//and re-translated
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
} else {
//multi-success
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
}
}
@@ -248,6 +265,7 @@ class AssetsController extends Controller
public function edit(Asset $asset) : View | RedirectResponse
{
$this->authorize($asset);
session()->put('back_url', url()->previous());
return view('hardware/edit')
->with('item', $asset)
->with('statuslabel_list', Helper::statusLabelList())
@@ -410,11 +428,15 @@ class AssetsController extends Controller
}
}
}
session()->put([
'redirect_option' => $request->get('redirect_option'),
'checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => $request->get('redirect_option') === 'other_redirect' ? 'model' : null,
]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->save()) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));
}
@@ -446,7 +468,7 @@ class AssetsController extends Controller
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
->update(['assigned_to' => null]);
->update(['assigned_to' => null, 'assigned_type' => null]);
}
@@ -458,6 +480,7 @@ class AssetsController extends Controller
}
}
$asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
@@ -519,7 +542,7 @@ class AssetsController extends Controller
{
$settings = Setting::getSettings();
if (($settings->qr_code == '1') && ($settings->label2_2d_type !== 'none')) {
if ($settings->label2_2d_type !== 'none') {
if ($asset) {
$size = Helper::barcodeDimensions($settings->label2_2d_type);
@@ -877,11 +900,20 @@ class AssetsController extends Controller
}
public function audit(Asset $asset)
public function audit(Asset $asset): View | RedirectResponse
{
$settings = Setting::getSettings();
$this->authorize('audit', Asset::class);
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
$settings = Setting::getSettings();
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
$dt = Carbon::now()->addMonths( (int) $settings->audit_interval)->toDateString();
return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list');
}
@@ -890,6 +922,10 @@ class AssetsController extends Controller
$this->authorize('audit', Asset::class);
session()->put('redirect_option', $request->get('redirect_option'));
session()->put('other_redirect', 'audit');
$originalValues = $asset->getRawOriginal();
$asset->next_audit_date = $request->input('next_audit_date');
@@ -924,8 +960,8 @@ class AssetsController extends Controller
}
}
// Validate custom fields
Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate();
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) {
@@ -965,7 +1001,7 @@ class AssetsController extends Controller
}
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
}
return redirect()->back()->withInput()->withErrors($asset->getErrors());

View File

@@ -52,11 +52,26 @@ class BulkAssetsController extends Controller
}
$asset_ids = $request->input('ids');
if ($request->input('bulk_actions') === 'checkout') {
$status_check =$this->hasUndeployableStatus($asset_ids);
if($status_check && $status_check['status'] === true){
$asset_tags = implode(', ', array_column($status_check['tags'], 'asset_tag'));
$asset_ids = $status_check['asset_ids'];
session()->flash('warning', trans('admin/hardware/message.undeployable', ['asset_tags' => $asset_tags]));
}
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('hardware.bulkcheckout.show');
}
if ($request->input('bulk_actions') === 'maintenance') {
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('maintenances.create');
}
// Figure out where we need to send the user after the update is complete, and store that in the session
$bulk_back_url = request()->headers->get('referer');
session(['bulk_back_url' => $bulk_back_url]);
@@ -97,11 +112,47 @@ class BulkAssetsController extends Controller
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
$assets = Asset::with('assignedTo', 'location', 'model')
$query = Asset::with('assignedTo', 'location', 'model')
->whereIn('assets.id', $asset_ids)
->withTrashed();
$assets = $assets->get();
switch ($sort_override) {
case 'model':
$query->OrderModels($order);
break;
case 'model_number':
$query->OrderModelNumber($order);
break;
case 'category':
$query->OrderCategory($order);
break;
case 'manufacturer':
$query->OrderManufacturer($order);
break;
case 'company':
$query->OrderCompany($order);
break;
case 'location':
$query->OrderLocation($order);
break;
case 'rtd_location':
$query->OrderRtdLocation($order);
break;
case 'status_label':
$query->OrderStatus($order);
break;
case 'supplier':
$query->OrderSupplier($order);
break;
case 'assigned_to':
$query->OrderAssigned($order);
break;
default:
$query->orderBy($column_sort, $order);
break;
}
$assets = $query->get();
if ($assets->isEmpty()) {
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
@@ -154,40 +205,7 @@ class BulkAssetsController extends Controller
}
}
switch ($sort_override) {
case 'model':
$assets->OrderModels($order);
break;
case 'model_number':
$assets->OrderModelNumber($order);
break;
case 'category':
$assets->OrderCategory($order);
break;
case 'manufacturer':
$assets->OrderManufacturer($order);
break;
case 'company':
$assets->OrderCompany($order);
break;
case 'location':
$assets->OrderLocation($order);
case 'rtd_location':
$assets->OrderRtdLocation($order);
break;
case 'status_label':
$assets->OrderStatus($order);
break;
case 'supplier':
$assets->OrderSupplier($order);
break;
case 'assigned_to':
$assets->OrderAssigned($order);
break;
default:
$assets->orderBy($column_sort, $order);
break;
}
return redirect()->back()->with('error', 'No action selected');
}
@@ -214,6 +232,21 @@ class BulkAssetsController extends Controller
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
// find custom field input attributes that start with 'null_'
$null_custom_fields_inputs = array_filter($request->all(), function ($key) {
// filter out all keys that start with 'null_'
return (strpos($key, 'null_') === 0);
}, ARRAY_FILTER_USE_KEY);;
// remove 'null' from the keys
$custom_fields_to_null = [];
foreach ($null_custom_fields_inputs as $key => $value) {
$custom_fields_to_null[str_replace('null', '', $key)] = $value;
}
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@@ -251,7 +284,9 @@ class BulkAssetsController extends Controller
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
|| ($request->filled('null_asset_eol_date'))
|| ($request->filled('null_notes'))
|| ($request->anyFilled($custom_field_columns))
|| ($request->anyFilled(array_keys($null_custom_fields_inputs)))
) {
// Let's loop through those assets and build an update array
@@ -274,10 +309,14 @@ class BulkAssetsController extends Controller
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date')
->conditionallyAddItem('asset_eol_date');
->conditionallyAddItem('asset_eol_date')
->conditionallyAddItem('notes');
foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column);
}
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
$this->conditionallyAddItem($key);
}
if (!($asset->eol_explicit)) {
if ($request->filled('model_id')) {
@@ -328,6 +367,10 @@ class BulkAssetsController extends Controller
}
}
if ($request->input('null_notes')=='1') {
$this->update_array['notes'] = null;
}
if ($request->filled('purchase_cost')) {
@@ -368,10 +411,12 @@ class BulkAssetsController extends Controller
// This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one.
if (
($asset->assigned_to == '')
|| ($updated_status->deployable == '1') && ($asset->assetstatus?->deployable == '1')
) {
$unassigned = $asset->assigned_to == '';
$deployable = $updated_status->deployable == '1' && $asset->assetstatus?->deployable == '1';
$pending = $updated_status->pending === 1;
if ($unassigned || $deployable || $pending) {
$this->update_array['status_id'] = $updated_status->id;
}
@@ -423,6 +468,7 @@ class BulkAssetsController extends Controller
}
/**
*
* Start all the custom fields shenanigans
*/
@@ -430,6 +476,15 @@ class BulkAssetsController extends Controller
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// null custom fields
if ($custom_fields_to_null) {
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
if ($field->db_column == $key) {
$this->update_array[$field->db_column] = null;
}
}
}
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
if (Gate::allows('admin')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
@@ -562,7 +617,10 @@ class BulkAssetsController extends Controller
public function showCheckout() : View
{
$this->authorize('checkout', Asset::class);
return view('hardware/bulk-checkout');
$do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::deployableStatusLabelList();
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
}
/**
@@ -594,13 +652,13 @@ class BulkAssetsController extends Controller
}
$checkout_at = date('Y-m-d H:i:s');
if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) {
$checkout_at = e($request->get('checkout_at'));
$checkout_at = $request->get('checkout_at');
}
$expected_checkin = '';
if ($request->filled('expected_checkin')) {
$expected_checkin = e($request->get('expected_checkin'));
$expected_checkin = $request->get('expected_checkin');
}
$errors = [];
@@ -608,6 +666,11 @@ class BulkAssetsController extends Controller
foreach ($assets as $asset) {
$this->authorize('checkout', $asset);
// See if there is a status label passed
if ($request->filled('status_id')) {
$asset->status_id = $request->get('status_id');
}
$checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null);
//TODO - I think this logic is duplicated in the checkOut method?
@@ -651,4 +714,25 @@ class BulkAssetsController extends Controller
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
}
}
public function hasUndeployableStatus (array $asset_ids)
{
$undeployable = Asset::whereIn('id', $asset_ids)
->undeployable()
->get();
$undeployableTags = $undeployable->map(function ($asset) {
return [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
];
})->toArray();
$undeployableIds = array_column($undeployableTags, 'id');
$filtered_ids = array_diff($asset_ids, $undeployableIds);
if($undeployable->isNotEmpty()) {
return ['status' => true, 'tags' => $undeployableTags, 'asset_ids' => $filtered_ids];
}
return false;
}
}

View File

@@ -484,6 +484,7 @@ class LoginController extends Controller
}
$request->session()->regenerate(true);
$request->session()->forget('2fa_authed');
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());

View File

@@ -68,6 +68,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
$category->created_by = auth()->id();
@@ -121,6 +122,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
@@ -145,7 +147,7 @@ class CategoriesController extends Controller
{
$this->authorize('delete', Category::class);
// Check if the category exists
if (is_null($category = Category::findOrFail($categoryId))) {
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
}
@@ -155,7 +157,6 @@ class CategoriesController extends Controller
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
// Redirect to the locations management page
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
}

View File

@@ -123,11 +123,13 @@ final class CompaniesController extends Controller
*/
public function destroy($companyId) : RedirectResponse
{
if (is_null($company = Company::find($companyId))) {
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found'));
}
$this->authorize('delete', $company);
if (! $company->isDeletable()) {
return redirect()->route('companies.index')

View File

@@ -100,8 +100,8 @@ class ComponentCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success',
trans('admin/components/message.checkin.success'));
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.checkin.success'));
}
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));

View File

@@ -120,6 +120,7 @@ class ComponentCheckoutController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.checkout.success'));
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.checkout.success'));
}
}

View File

@@ -91,7 +91,8 @@ class ComponentsController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) {
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.create.success'));
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -111,6 +112,7 @@ class ComponentsController extends Controller
{
$this->authorize('update', $component);
session()->put('back_url', url()->previous());
return view('components/edit')
->with('item', $component)
->with('category_type', 'component');
@@ -164,7 +166,8 @@ class ComponentsController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) {
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.update.success'));
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());

View File

@@ -111,6 +111,7 @@ class ConsumableCheckoutController extends Controller
// Redirect to the new consumable page
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.checkout.success'));
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.checkout.success'));
}
}

View File

@@ -90,7 +90,8 @@ class ConsumablesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.create.success'));
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -107,6 +108,7 @@ class ConsumablesController extends Controller
public function edit(Consumable $consumable) : View | RedirectResponse
{
$this->authorize($consumable);
session()->put('back_url', url()->previous());
return view('consumables/edit')
->with('item', $consumable)
->with('category_type', 'consumable');
@@ -160,7 +162,8 @@ class ConsumablesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.update.success'));
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());

View File

@@ -83,30 +83,30 @@ class CustomFieldsController extends Controller
{
$this->authorize('create', CustomField::class);
$show_in_email = $request->get("show_in_email", 0);
$display_in_user_view = $request->get("display_in_user_view", 0);
$show_in_email = $request->input("show_in_email", 0);
$display_in_user_view = $request->input("display_in_user_view", 0);
// Override the display settings if the field is encrypted
if ($request->get("field_encrypted") == '1') {
if ($request->input("field_encrypted") == '1') {
$show_in_email = '0';
$display_in_user_view = '0';
}
$field = new CustomField([
"name" => trim($request->get("name")),
"element" => $request->get("element"),
"help_text" => $request->get("help_text"),
"field_values" => $request->get("field_values"),
"field_encrypted" => $request->get("field_encrypted", 0),
"name" => trim($request->input("name")),
"element" => $request->input("element"),
"help_text" => $request->input("help_text"),
"field_values" => $request->input("field_values"),
"field_encrypted" => $request->input("field_encrypted", 0),
"show_in_email" => $show_in_email,
"is_unique" => $request->get("is_unique", 0),
"is_unique" => $request->input("is_unique", 0),
"display_in_user_view" => $display_in_user_view,
"auto_add_to_fieldsets" => $request->get("auto_add_to_fieldsets", 0),
"show_in_listview" => $request->get("show_in_listview", 0),
"show_in_requestable_list" => $request->get("show_in_requestable_list", 0),
"display_checkin" => $request->get("display_checkin", 0),
"display_checkout" => $request->get("display_checkout", 0),
"display_audit" => $request->get("display_audit", 0),
"auto_add_to_fieldsets" => $request->input("auto_add_to_fieldsets", 0),
"show_in_listview" => $request->input("show_in_listview", 0),
"show_in_requestable_list" => $request->input("show_in_requestable_list", 0),
"display_checkin" => $request->input("display_checkin", 0),
"display_checkout" => $request->input("display_checkout", 0),
"display_audit" => $request->input("display_audit", 0),
"created_by" => auth()->id()
]);
@@ -238,8 +238,8 @@ class CustomFieldsController extends Controller
$display_in_user_view = '0';
}
$field->name = trim(e($request->get("name")));
$field->element = e($request->get("element"));
$field->name = trim($request->get("name"));
$field->element = $request->get("element");
$field->field_values = $request->get("field_values");
$field->created_by = auth()->id();
$field->help_text = $request->get("help_text");
@@ -254,9 +254,9 @@ class CustomFieldsController extends Controller
$field->display_audit = $request->get("display_audit", 0);
if ($request->get('format') == 'CUSTOM REGEX') {
$field->format = e($request->get('custom_format'));
$field->format = $request->get('custom_format');
} else {
$field->format = e($request->get('format'));
$field->format = $request->get('format');
}
if ($field->element == 'checkbox' || $field->element == 'radio'){

View File

@@ -86,7 +86,7 @@ class LicenseCheckinController extends Controller
}
if($licenseSeat->assigned_to != null){
$return_to = User::find($licenseSeat->assigned_to);
$return_to = User::withTrashed()->find($licenseSeat->assigned_to);
session()->put('checkedInFrom', $return_to->id);
} else {
$return_to = Asset::find($licenseSeat->asset_id);
@@ -98,14 +98,17 @@ 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()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success'));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkin.success'));
}
// Redirect to the license page with error

View File

@@ -89,7 +89,8 @@ class LicenseCheckoutController extends Controller
if ($checkoutTarget) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkout.success'));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkout.success'));
}

View File

@@ -105,7 +105,8 @@ class LicensesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($license->save()) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.create.success'));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($license->getErrors());
@@ -125,7 +126,7 @@ class LicensesController extends Controller
{
$this->authorize('update', $license);
session()->put('back_url', url()->previous());
$maintained_list = [
'' => 'Maintained',
'1' => 'Yes',
@@ -181,7 +182,8 @@ class LicensesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($license->save()) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.update.success'));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.update.success'));
}
// If we can't adjust the number of seats, the error is flashed to the session by the event handler in License.php
return redirect()->back()->withInput()->withErrors($license->getErrors());

View File

@@ -3,15 +3,21 @@
namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\ProfileTransformer;
use App\Models\Actionlog;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This controller handles all actions related to User Profiles for
* the Snipe-IT Asset Management application.
@@ -220,7 +226,7 @@ class ProfileController extends Controller
if (!$user = User::find(auth()->id())) {
return redirect()->back()
->with('error', trans('admin/users/message.user_not_found', ['id' => $id]));
->with('error', trans('admin/users/message.user_not_found', ['id' => auth()->id()]));
}
if (empty($user->email)) {
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
@@ -234,4 +240,28 @@ class ProfileController extends Controller
return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
}
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
{
$logentry = Actionlog::where('filename', $filename)->first();
// Make sure the user has permission to view this file
if (auth()->id() != $logentry->target_id) {
return redirect()->route('account')->with('error', trans('general.generic_model_not_found', ['model' => 'file']));
}
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
}
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
}
}

View File

@@ -184,7 +184,7 @@ class ReportsController extends Controller
$currency = e(Setting::getSettings()->default_currency);
}
$row[] = $asset->purchase_date;
$row[] = Helper::getFormattedDateObject($asset->purchase_date, 'date', false);
$row[] = $currency.Helper::formatCurrencyOutput($asset->purchase_cost);
$row[] = $currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue());
$row[] = $currency.Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue()));
@@ -485,7 +485,7 @@ class ReportsController extends Controller
$header[] = trans('admin/hardware/table.purchase_date');
}
if (($request->filled('purchase_cost')) || ($request->filled('depreciation'))) {
if ($request->filled('purchase_cost')) {
$header[] = trans('admin/hardware/table.purchase_cost');
}
@@ -737,6 +737,11 @@ class ReportsController extends Controller
if (($request->filled('next_audit_start')) && ($request->filled('next_audit_end'))) {
$assets->whereBetween('assets.next_audit_date', [$request->input('next_audit_start'), $request->input('next_audit_end')]);
}
if (($request->filled('last_updated_start')) && ($request->filled('last_updated_end'))) {
$assets->whereBetween('assets.updated_at', [$request->input('last_updated_start'), $request->input('last_updated_end')]);
}
if ($request->filled('exclude_archived')) {
$assets->notArchived();
}

View File

@@ -14,6 +14,7 @@ use App\Http\Requests\StoreLabelSettings;
use App\Http\Requests\StoreSecuritySettings;
use App\Models\CustomField;
use App\Models\Group;
use App\Models\Labels\Label as LabelModel;
use App\Models\Setting;
use App\Models\Asset;
use App\Models\User;
@@ -290,8 +291,7 @@ class SettingsController extends Controller
public function getSettings() : View
{
$setting = Setting::getSettings();
$total_locations = count(Helper::test_locations_fmcs(false));
return view('settings/general', compact('setting'))->with('total_locations', $total_locations);
return view('settings/general', compact('setting'));
}
/**
@@ -352,6 +352,7 @@ class SettingsController extends Controller
$setting->dash_chart_type = $request->input('dash_chart_type');
$setting->profile_edit = $request->input('profile_edit', 0);
$setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0);
$setting->manager_view_enabled = $request->input('manager_view_enabled', 0);
if ($request->input('per_page') != '') {
@@ -650,6 +651,7 @@ class SettingsController extends Controller
$setting->alert_email = $alert_email;
$setting->admin_cc_email = $admin_cc_email;
$setting->admin_cc_always = $request->validated('admin_cc_always');
$setting->alerts_enabled = $request->input('alerts_enabled', '0');
$setting->alert_interval = $request->input('alert_interval');
$setting->alert_threshold = $request->input('alert_threshold');
@@ -772,6 +774,7 @@ class SettingsController extends Controller
$setting->label2_2d_type = $request->input('label2_2d_type');
$setting->label2_2d_target = $request->input('label2_2d_target');
$setting->label2_fields = $request->input('label2_fields');
$setting->label2_empty_row_count = $request->input('label2_empty_row_count');
$setting->labels_per_page = $request->input('labels_per_page');
$setting->labels_width = $request->input('labels_width');
$setting->labels_height = $request->input('labels_height');
@@ -921,7 +924,7 @@ class SettingsController extends Controller
* @since v5.0.0
*/
public function postSamlSettings(SettingsSamlRequest $request) : RedirectResponse
{
{
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}

View File

@@ -154,7 +154,8 @@ class UsersController extends Controller
$user->notify(new WelcomeNotification($data));
}
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))->with('success', trans('admin/users/message.success.create'));
return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.create'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -178,7 +179,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $permissions
* @return \Illuminate\Contracts\View\View
* @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @internal param int $id
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
@@ -186,10 +187,15 @@ class UsersController extends Controller
{
$this->authorize('update', User::class);
session()->put('back_url', url()->previous());
$user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($user->id);
if ($user) {
if ($user->trashed()) {
return redirect()->route('users.show', $user->id);
}
$permissions = config('permissions');
$groups = Group::pluck('name', 'id');
@@ -308,7 +314,7 @@ class UsersController extends Controller
if ($user->save()) {
// Redirect to the user page
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))
return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -506,6 +512,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'),
@@ -551,6 +559,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

@@ -27,50 +27,126 @@ use Exception;
class ViewAssetsController extends Controller
{
/**
* Redirect to the profile page.
* Extract custom fields that should be displayed in user view.
*
* @param User $user
* @return array
*/
private function extractCustomFields(User $user): array
{
$fieldArray = [];
foreach ($user->assets as $asset) {
if ($asset->model && $asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
if ($field->display_in_user_view == '1') {
$fieldArray[$field->db_column] = $field->name;
}
}
}
}
return array_unique($fieldArray);
}
/**
* Get list of users viewable by the current user.
*
* @param User $authUser
* @return \Illuminate\Support\Collection
*/
private function getViewableUsers(User $authUser): \Illuminate\Support\Collection
{
// SuperAdmin sees all users
if ($authUser->isSuperUser()) {
return User::select('id', 'first_name', 'last_name', 'username')
->where('activated', 1)
->orderBy('last_name')
->orderBy('first_name')
->get();
}
// Regular manager sees only their subordinates + self
$managedUsers = $authUser->getAllSubordinates();
// If user has subordinates, show them with self at beginning
if ($managedUsers->count() > 0) {
return collect([$authUser])->merge($managedUsers)
->sortBy('last_name')
->sortBy('first_name');
}
// User has no subordinates, only sees themselves
return collect([$authUser]);
}
/**
* Get the selected user ID from request or default to current user.
*
* @param Request $request
* @param \Illuminate\Support\Collection $subordinates
* @param int $defaultUserId
* @return int
*/
private function getSelectedUserId(Request $request, \Illuminate\Support\Collection $subordinates, int $defaultUserId): int
{
// If no subordinates or no user_id in request, return default
if ($subordinates->count() <= 1 || !$request->filled('user_id')) {
return $defaultUserId;
}
$requestedUserId = (int) $request->input('user_id');
// Validate if the requested user is allowed
if ($subordinates->contains('id', $requestedUserId)) {
return $requestedUserId;
}
// If invalid ID or not authorized, return default
return $defaultUserId;
}
/**
* Show user's assigned assets with optional manager view functionality.
*
*/
public function getIndex() : View | RedirectResponse
public function getIndex(Request $request) : View | RedirectResponse
{
$user = User::with(
$authUser = auth()->user();
$settings = Setting::getSettings();
$subordinates = collect();
$selectedUserId = $authUser->id;
// Process manager view if enabled
if ($settings->manager_view_enabled) {
$subordinates = $this->getViewableUsers($authUser);
$selectedUserId = $this->getSelectedUserId($request, $subordinates, $authUser->id);
}
// Load the data for the user to be viewed (either auth user or selected subordinate)
$userToView = User::with([
'assets',
'assets.model',
'assets.model.fieldset.fields',
'consumables',
'accessories',
'licenses',
)->find(auth()->id());
$field_array = array();
// Loop through all the custom fields that are applied to any model the user has assigned
foreach ($user->assets as $asset) {
// Make sure the model has a custom fieldset before trying to loop through the associated fields
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// check and make sure they're allowed to see the value of the custom field
if ($field->display_in_user_view == '1') {
$field_array[$field->db_column] = $field->name;
}
}
}
'licenses'
])->find($selectedUserId);
// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
if (!$userToView) {
return redirect()->route('view-assets')->with('error', trans('admin/users/message.user_not_found'));
}
// Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns
array_unique($field_array);
// Process custom fields for the user being viewed
$fieldArray = $this->extractCustomFields($userToView);
if (isset($user->id)) {
return view('account/view-assets', compact('user', 'field_array' ))
->with('settings', Setting::getSettings());
}
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', $user->id));
// Pass the necessary data to the view
return view('account/view-assets', [
'user' => $userToView, // Use 'user' for compatibility with the existing view
'field_array' => $fieldArray,
'settings' => $settings,
'subordinates' => $subordinates,
'selectedUserId' => $selectedUserId
]);
}
/**

View File

@@ -73,6 +73,7 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'api-throttle' => \App\Http\Middleware\SetAPIResponseHeaders::class,
'health' => null,
];
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Symfony\Component\HttpFoundation\Response;
class SetAPIResponseHeaders extends ThrottleRequests
{
/**
* Add the rate limit headers to the response.
*
* This extends the original ThrottleRequests middleware to add the 'X-RateLimit-Reset' and 'Retry-After' headers, even
* if the rate limit is not exceeded.
* @param $maxAttempts
* @param $remainingAttempts
* @param $retryAfter
* @param Response|null $response
* @return array|int[]
*/
protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null, ?Response $response = null)
{
if ($response &&
! is_null($response->headers->get('X-RateLimit-Remaining')) &&
(int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
$headers = [];
$headers['Retry-After'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
return $headers;
}
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (! is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
}
return $headers;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decaySeconds);
}
$response = $next($request);
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts),
$this->getTimeUntilNextRetry($limit->key) // this is the only line we changed
);
}
return $response;
}
}

View File

@@ -41,6 +41,7 @@ class SettingsSamlRequest extends FormRequest
public function withValidator($validator)
{
$validator->after(function ($validator) {
$setting = Setting::getSettings();
if ($this->input('saml_enabled') == '1') {
$idpMetadata = $this->input('saml_idp_metadata');
if (! empty($idpMetadata)) {
@@ -56,7 +57,7 @@ class SettingsSamlRequest extends FormRequest
}
}
$was_custom_x509cert = strpos(Setting::getSettings()->saml_custom_settings, 'sp_x509cert') !== false;
$was_custom_x509cert = strpos($setting->saml_custom_settings, 'sp_x509cert') !== false;
$custom_x509cert = '';
$custom_privateKey = '';
@@ -126,10 +127,14 @@ class SettingsSamlRequest extends FormRequest
}
if (! (empty($x509cert) && empty($privateKey))) {
$this->merge([
'saml_sp_x509cert' => $x509cert,
'saml_sp_privatekey' => $privateKey,
]);
// $this->merge([
// 'saml_sp_x509cert' => $x509cert,
// 'saml_sp_privatekey' => $privateKey,
// ]);
$setting->saml_sp_x509cert = $x509cert;
$setting->saml_sp_privatekey = $privateKey;
$setting->save();
}
} else {
$validator->errors()->add('saml_integration', 'openssl.cnf is missing/invalid');
@@ -145,15 +150,21 @@ class SettingsSamlRequest extends FormRequest
}
if (! empty($x509certNew)) {
$this->merge([
'saml_sp_x509certNew' => $x509certNew,
]);
// $this->merge([
// 'saml_sp_x509certNew' => $x509certNew,
// ]);
$setting->saml_sp_x509certNew = $x509certNew;
$setting->save();
}
} else {
$this->merge([
'saml_sp_x509certNew' => '',
]);
// $this->merge([
// 'saml_sp_x509certNew' => '',
// ]);
$setting->saml_sp_x509certNew = '';
$setting->save();
}
});
}
}

View File

@@ -19,6 +19,7 @@ class StoreAssetModelRequest extends ImageUploadRequest
public function prepareForValidation(): void
{
parent::prepareForValidation();
if ($this->category_id) {
if ($category = Category::find($this->category_id)) {

View File

@@ -39,7 +39,6 @@ class StoreAssetRequest extends ImageUploadRequest
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
'assigned_to' => $assigned_to ?? null,
]);
}

View File

@@ -37,8 +37,8 @@ class StoreLabelSettings extends FormRequest
return [
'labels_per_page' => 'numeric',
'labels_width' => 'numeric',
'labels_height' => 'numeric',
'labels_width' => 'numeric|min:0.1',
'labels_height' => 'numeric|min:0.1',
'labels_pmargin_left' => 'numeric|nullable',
'labels_pmargin_right' => 'numeric|nullable',
'labels_pmargin_top' => 'numeric|nullable',

View File

@@ -5,6 +5,7 @@ namespace App\Http\Requests;
use App\Models\Accessory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class StoreNotificationSettings extends FormRequest
{
@@ -26,6 +27,9 @@ class StoreNotificationSettings extends FormRequest
return [
'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email_array|nullable',
'admin_cc_always' => [
Rule::in('0', '1'),
],
'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable|gt:0',
'audit_warning_days' => 'numeric|nullable',

View File

@@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use \App\Helpers\Helper;
class UploadFileRequest extends Request
{
@@ -27,44 +28,65 @@ class UploadFileRequest extends Request
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
$max_file_size = Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size,
'file.*' => 'required|mimes:'.config('filesystems.allowed_upload_extensions_for_validator').'|max:'.$max_file_size,
];
}
/**
* Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename
*
* TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
* a way to merge them or extend one into the other?
*/
public function handleFile(string $dirname, string $name_prefix, $file): string
{
$extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
// Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') {
Log::debug('This is an SVG');
Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put($dirname.$file_name, $cleanSVG);
} catch (\Exception $e) {
Log::debug('Upload no workie :( ');
Log::debug($e);
}
$uploaded_file = $this->handleSVG($file);
} else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file));
$uploaded_file = file_get_contents($file);
}
try {
Storage::put($dirname.$file_name, $uploaded_file);
} catch (\Exception $e) {
Log::debug($e);
}
return $file_name;
}
}
public function handleSVG($file)
{
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
return $sanitizer->sanitize($dirtySVG);
}
/**
* Get the validation error messages that apply to the request, but
* replace the attribute name with the name of the file that was attempted and failed
* to make it clearer to the user which file is the bad one.
*
* @return array
*/
public function attributes(): array
{
$attributes = [];
if ($this->file) {
for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
}
return $attributes;
}
}

View File

@@ -113,8 +113,8 @@ class ActionlogsTransformer
// Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old): '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new): '';
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old, ['allowed_classes' => false]) : '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new, ['allowed_classes' => false]) : '';
}
}
@@ -198,8 +198,8 @@ class ActionlogsTransformer
'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null,
'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null,
'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null,
'remote_ip' => ($actionlog->remote_ip) ?? null,
'user_agent' => ($actionlog->user_agent) ?? null,
'remote_ip' => e($actionlog->remote_ip) ?? null,
'user_agent' => e($actionlog->user_agent) ?? null,
'action_source' => ($actionlog->action_source) ?? null,
'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'),
];

View File

@@ -59,7 +59,10 @@ class AssetMaintenancesTransformer
'name'=> e($assetmaintenance->asset->defaultLoc->name),
] : null,
'notes' => ($assetmaintenance->notes) ? Helper::parseEscapedMarkedownInline($assetmaintenance->notes) : null,
'supplier' => ($assetmaintenance->supplier) ? ['id' => $assetmaintenance->supplier->id, 'name'=> e($assetmaintenance->supplier->name)] : null,
'supplier' => ($assetmaintenance->supplier) ? [
'id' => $assetmaintenance->supplier->id,
'name'=> e($assetmaintenance->supplier->name)
] : null,
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),

View File

@@ -80,6 +80,7 @@ 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) ? [
@@ -101,7 +102,7 @@ class AssetsTransformer
'checkout_counter' => (int) $asset->checkout_counter,
'requests_counter' => (int) $asset->requests_counter,
'user_can_checkout' => (bool) $asset->availableForCheckout(),
'book_value' => Helper::formatCurrencyOutput($asset->getLinearDepreciatedValue()),
'book_value' => Helper::formatCurrencyOutput($asset->getDepreciatedValue()),
];
@@ -155,6 +156,7 @@ class AssetsTransformer
'clone' => Gate::allows('create', Asset::class) ? true : false,
'restore' => ($asset->deleted_at!='' && Gate::allows('create', Asset::class)) ? true : false,
'update' => ($asset->deleted_at=='' && Gate::allows('update', Asset::class)) ? true : false,
'audit' => Gate::allows('audit', Asset::class) ? true : false,
'delete' => ($asset->deleted_at=='' && $asset->assigned_to =='' && Gate::allows('delete', Asset::class) && ($asset->deleted_at == '')) ? true : false,
];

View File

@@ -4,6 +4,10 @@ namespace App\Http\Transformers;
class DatatablesTransformer
{
/**
* Transform data for bootstrap tables and API responses for lists of things
**/
public function transformDatatables($objects, $total = null)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
@@ -11,4 +15,15 @@ class DatatablesTransformer
return $objects_array;
}
}
/**
* Transform data for returning the status of items within a bulk action
**/
public function transformBulkResponseWithStatusAndObjects($objects, $total)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
}

View File

@@ -26,11 +26,11 @@ class DepreciationsTransformer
$array = [
'id' => (int) $depreciation->id,
'name' => e($depreciation->name),
'months' => $depreciation->months.' '.trans('general.months'),
'months' => trans_choice('general.months_plural', $depreciation->months),
'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min,
'assets_count' => $depreciation->assets_count,
'models_count' => $depreciation->models_count,
'licenses_count' => $depreciation->licenses_count,
'assets_count' => ($depreciation->assets_count > 0) ? (int) $depreciation->assets_count : 0,
'models_count' => ($depreciation->models_count > 0) ? (int) $depreciation->models_count : 0,
'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0,
'created_by' => ($depreciation->adminuser) ? [
'id' => (int) $depreciation->adminuser->id,
'name'=> e($depreciation->adminuser->present()->fullName()),

View File

@@ -13,16 +13,15 @@ class LicenseSeatsTransformer
public function transformLicenseSeats(Collection $seats, $total)
{
$array = [];
$seat_count = 0;
foreach ($seats as $seat) {
$seat_count++;
$array[] = self::transformLicenseSeat($seat, $seat_count);
$array[] = self::transformLicenseSeat($seat);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformLicenseSeat(LicenseSeat $seat, $seat_count = 0)
public function transformLicenseSeat(LicenseSeat $seat)
{
$array = [
'id' => (int) $seat->id,
@@ -55,10 +54,6 @@ class LicenseSeatsTransformer
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
];
if ($seat_count != 0) {
$array['name'] = trans('admin/licenses/general.seat_count', ['count' => $seat_count]);
}
$permissions_array['available_actions'] = [
'checkout' => Gate::allows('checkout', License::class),
'checkin' => Gate::allows('checkin', License::class),

View File

@@ -57,6 +57,10 @@ class LocationsTransformer
'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null,
'notes' => Helper::parseEscapedMarkedownInline($location->notes),
'created_at' => Helper::getFormattedDateObject($location->created_at, 'datetime'),
'created_by' => $location->adminuser ? [
'id' => (int) $location->adminuser->id,
'name'=> e($location->adminuser->present()->fullName),
]: null,
'updated_at' => Helper::getFormattedDateObject($location->updated_at, 'datetime'),
'parent' => ($location->parent) ? [
'id' => (int) $location->parent->id,

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Collection;
class ProfileTransformer
{
public function transformFiles(Collection $files, $total)
{
$array = [];
foreach ($files as $file) {
$array[] = self::transformFile($file);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformFile(Actionlog $file)
{
$array = [
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->getFullNameAttribute()) : e($file->item->getDisplayNameAttribute()),
'type' => e($file->itemType()),
] : null,
'filename' => e($file->filename),
'signature_file' => ($file->accept_signature) ? route('profile.signature.view', ['filename' => $file->accept_signature ]) : null,
'note' => e($file->note),
'url' => route('profile.storedeula.download', ['filename' => $file->filename]),
'file' => route('profile.storedeula.download', ['filename' => $file->filename]),
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
];
return $array;
}
}

View File

@@ -45,6 +45,10 @@ class SuppliersTransformer
'components_count' => (int) $supplier->components_count,
'notes' => ($supplier->notes) ? Helper::parseEscapedMarkedownInline($supplier->notes) : null,
'created_at' => Helper::getFormattedDateObject($supplier->created_at, 'datetime'),
'created_by' => $supplier->adminuser ? [
'id' => (int) $supplier->adminuser->id,
'name'=> e($supplier->adminuser->present()->fullName),
]: null,
'updated_at' => Helper::getFormattedDateObject($supplier->updated_at, 'datetime'),
];

View File

@@ -3,10 +3,10 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class UploadedFilesTransformer
@@ -26,23 +26,26 @@ class UploadedFilesTransformer
{
$snipeModel = $file->item_type;
// This will be used later as we extend out this transformer to handle more types of uploads
if ($file->item_type == Asset::class) {
$file_url = route('show/assetfile', [$file->item_id, $file->id]);
}
$array = [
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
] : null,
'filename' => e($file->filename),
'url' => $file_url,
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [
'id' => (int) $file->adminuser->id,
'name'=> e($file->adminuser->present()->fullName),
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($file->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
];
$permissions_array['available_actions'] = [
@@ -53,4 +56,5 @@ class UploadedFilesTransformer
return $array;
}
}
}

View File

@@ -50,6 +50,10 @@ class UsersTransformer
'id' => (int) $user->department->id,
'name'=> e($user->department->name),
] : null,
'department_manager' => ($user->department?->manager) ? [
'id' => (int) $user->department->manager->id,
'name'=> e($user->department->manager->full_name),
] : null,
'location' => ($user->userloc) ? [
'id' => (int) $user->userloc->id,
'name'=> e($user->userloc->name),

View File

@@ -80,7 +80,16 @@ class AssetImporter extends ItemImporter
$asset_tag = Asset::autoincrement_asset();
}
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override asset if an ID was given
\Log::debug('Finding asset by ID: '.$this->findCsvMatch($row, 'id'));
$asset = Asset::find($this->findCsvMatch($row, 'id'));
} else {
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
}
if ($asset) {
if (! $this->updating) {
$exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]);

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Importer;
use App\Models\Category;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class CategoryImporter
*/
class CategoryImporter extends ItemImporter
{
protected $categories;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createCategoryIfNotExists($row);
}
/**
* Create a category if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createCategoryIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createCategoryIfNotExists(array $row)
{
$editingCategory = false;
$category = Category::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override category if an ID was given
\Log::debug('Finding category by ID: '.$this->findCsvMatch($row, 'id'));
$category = Category::find($this->findCsvMatch($row, 'id'));
}
if ($category) {
if (! $this->updating) {
$this->log('A matching Category '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Category');
$editingCategory = true;
} else {
$this->log('No Matching Category, Create a new one');
$category = new Category;
$category->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
$this->item['eula_text'] = trim($this->findCsvMatch($row, 'eula_text'));
$this->item['category_type'] = trim(strtolower($this->findCsvMatch($row, 'category_type')));
$this->item['use_default_eula'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'use_default_eula'))) == 1) ? 1 : 0;
$this->item['require_acceptance'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_acceptance'))) == 1) ? 1 : 0;
$this->item['checkin_email'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'checkin_email'))) == 1) ? 1 : 0;
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingCategory) {
Log::debug('Updating existing category');
$category->update($this->sanitizeItemForUpdating($category));
} else {
Log::debug('Creating category');
$category->fill($this->sanitizeItemForStoring($category));
}
if ($category->save()) {
$this->log('Category '.$category->name.' created or updated from CSV import');
return $category;
} else {
Log::debug($category->getErrors());
$this->logError($category, 'Category "'.$this->item['name'].'"');
return $category->errors;
}
}
}

View File

@@ -88,6 +88,7 @@ abstract class Importer
'department' => 'department',
'manager_name' => 'manager full name',
'manager_username' => 'manager username',
'manager_employee_num' => 'manager employee number',
'min_amt' => 'minimum quantity',
'remote' => 'remote',
'vip' => 'vip',

View File

@@ -110,7 +110,7 @@ class ItemImporter extends Importer
protected function determineCheckout($row)
{
// Locations don't get checked out to anyone/anything
if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class)) {
if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class) || (get_class($this) == SupplierImporter::class) || (get_class($this) == ManufacturerImporter::class) || (get_class($this) == CategoryImporter::class)) {
return;
}
@@ -353,16 +353,27 @@ class ItemImporter extends Importer
* @param $user_manager string
* @return int id of company created/found
*/
public function fetchManager($user_manager_first_name, $user_manager_last_name)
public function fetchManager($user_manager_username = null, $user_manager_employee_num = null, $user_manager_first_name = null, $user_manager_last_name = null)
{
$manager = User::where('first_name', '=', $user_manager_first_name)
->where('last_name', '=', $user_manager_last_name)->first();
if ($user_manager_username!='') {
$manager = User::where('username', '=', $user_manager_username)->first();
$this->log('Checking on username '.$user_manager_username);
} elseif ($user_manager_employee_num!='') {
$manager = User::where('employee_num', '=', $user_manager_employee_num)->first();
$this->log('Checking on employee_num '.$user_manager_employee_num);
} else {
$manager = User::where('first_name', '=', $user_manager_first_name)
->where('last_name', '=', $user_manager_last_name)->first();
$this->log('Checking on full name');
}
if ($manager) {
$this->log('A matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' already exists');
return $manager->id;
}
$this->log('No matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' found. If their user account is being created through this import, you should re-process this file again. ');
$this->log('No matching Manager found. If their user account is being created through this import, you should re-process this file again. ');
return null;
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Importer;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class ManufacturerImporter
*/
class ManufacturerImporter extends ItemImporter
{
protected $manufacturers;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createManufacturerIfNotExists($row);
}
/**
* Create a supplier if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createManufacturerIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createManufacturerIfNotExists(array $row)
{
$editingManufacturer = false;
$supplier = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override supplier if an ID was given
\Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id'));
$supplier = Manufacturer::find($this->findCsvMatch($row, 'id'));
}
if ($supplier) {
if (! $this->updating) {
$this->log('A matching Manufacturer '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Manufacturer');
$editingManufacturer = true;
} else {
$this->log('No Matching Manufacturer, Create a new one');
$supplier = new Manufacturer;
$supplier->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['support_phone'] = trim($this->findCsvMatch($row, 'support_phone'));
$this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
$this->item['support_email'] = trim($this->findCsvMatch($row, 'support_email'));
$this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
$this->item['url'] = trim($this->findCsvMatch($row, 'url'));
$this->item['support_url'] = trim($this->findCsvMatch($row, 'support_url'));
$this->item['warranty_lookup_url'] = trim($this->findCsvMatch($row, 'warranty_lookup_url'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingManufacturer) {
Log::debug('Updating existing supplier');
$supplier->update($this->sanitizeItemForUpdating($supplier));
} else {
Log::debug('Creating supplier');
$supplier->fill($this->sanitizeItemForStoring($supplier));
}
if ($supplier->save()) {
$this->log('Manufacturer '.$supplier->name.' created or updated from CSV import');
return $supplier;
} else {
Log::debug($supplier->getErrors());
$this->logError($supplier, 'Manufacturer "'.$this->item['name'].'"');
return $supplier->errors;
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Importer;
use App\Models\Supplier;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class SupplierImporter
*/
class SupplierImporter extends ItemImporter
{
protected $suppliers;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createSupplierIfNotExists($row);
}
/**
* Create a supplier if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createSupplierIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createSupplierIfNotExists(array $row)
{
$editingSupplier = false;
$supplier = Supplier::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override supplier if an ID was given
\Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id'));
$supplier = Supplier::find($this->findCsvMatch($row, 'id'));
}
if ($supplier) {
if (! $this->updating) {
$this->log('A matching Supplier '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Supplier');
$editingSupplier = true;
} else {
$this->log('No Matching Supplier, Create a new one');
$supplier = new Supplier;
$supplier->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['address'] = trim($this->findCsvMatch($row, 'address'));
$this->item['address2'] = trim($this->findCsvMatch($row, 'address2'));
$this->item['city'] = trim($this->findCsvMatch($row, 'city'));
$this->item['state'] = trim($this->findCsvMatch($row, 'state'));
$this->item['country'] = trim($this->findCsvMatch($row, 'country'));
$this->item['zip'] = trim($this->findCsvMatch($row, 'zip'));
$this->item['phone'] = trim($this->findCsvMatch($row, 'phone'));
$this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
$this->item['email'] = trim($this->findCsvMatch($row, 'email'));
$this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
$this->item['url'] = trim($this->findCsvMatch($row, 'url'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingSupplier) {
Log::debug('Updating existing supplier');
$supplier->update($this->sanitizeItemForUpdating($supplier));
} else {
Log::debug('Creating supplier');
$supplier->fill($this->sanitizeItemForStoring($supplier));
}
if ($supplier->save()) {
$this->log('Supplier '.$supplier->name.' created or updated from CSV import');
return $supplier;
} else {
Log::debug($supplier->getErrors());
$this->logError($supplier, 'Supplier "'.$this->item['name'].'"');
return $supplier->errors;
}
}
}

View File

@@ -62,7 +62,7 @@ class UserImporter extends ItemImporter
$this->item['activated'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'activated'))) == 1) ? '1' : 0;
$this->item['employee_num'] = trim($this->findCsvMatch($row, 'employee_num'));
$this->item['department_id'] = trim($this->createOrFetchDepartment(trim($this->findCsvMatch($row, 'department'))));
$this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
$this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_username')), trim($this->findCsvMatch($row, 'manager_employee_num')), trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
$this->item['remote'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'remote'))) == 1 ) ? '1' : 0;
$this->item['vip'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'vip'))) ==1 ) ? '1' : 0;
$this->item['autoassign_licenses'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'autoassign_licenses'))) ==1 ) ? '1' : 0;

View File

@@ -12,6 +12,7 @@ use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Category;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\Consumable;
@@ -27,6 +28,7 @@ use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Exception;
@@ -40,6 +42,24 @@ class CheckoutableListener
Component::class,
];
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
*/
public function subscribe($events)
{
$events->listen(
\App\Events\CheckoutableCheckedIn::class,
'App\Listeners\CheckoutableListener@onCheckedIn'
);
$events->listen(
\App\Events\CheckoutableCheckedOut::class,
'App\Listeners\CheckoutableListener@onCheckedOut'
);
}
/**
* Notify the user and post to webhook about the checked out checkoutable
* and add a record to the checkout_requests table.
@@ -50,93 +70,70 @@ class CheckoutableListener
return;
}
/**
* Make a checkout acceptance and attach it in the notification
*/
$settings = Setting::getSettings();
$acceptance = $this->getCheckoutAcceptance($event);
$adminCcEmailsArray = [];
if ($settings->admin_cc_email !== '') {
$adminCcEmail = $settings->admin_cc_email;
$adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail));
$shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable);
$shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance);
$shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return;
}
$ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckoutMailType($event, $acceptance);
$notifiable = $this->getNotifiableUsers($event);
if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
$mailable = $this->getCheckoutMailType($event, $acceptance);
$notifiable = $this->getNotifiableUser($event);
// Send email notifications
try {
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
* 4. If the admin CC email is set, even if the item being checked out doesn't have an email address (location, etc)
*/
$notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
$this->checkoutableShouldSendEmail($event)) {
$shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
[$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
// Send a checkout email to the admin CC addresses, even if the target has no email
if (!empty($ccEmails)) {
Mail::to($ccEmails)->send($mailable);
Log::info('Checkout Mail sent to CC addresses');
}
// Send a checkout email to the target if it has an email
if (!empty($notifiable->email)) {
Mail::to($notifiable)->send($mailable);
if (!empty($to)) {
try {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
Log::info('Checkout Mail sent to checkout target');
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
// Send notification
try {
if ($this->shouldSendWebhookNotification()) {
if ($shouldSendWebhookNotification) {
try {
if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckoutNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
} else {
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event, $acceptance));
}
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
} else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
}
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') );
}
else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
}
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail') );
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
}
/**
* Notify the user and post to webhook about the checked in checkoutable
*/
*/
public function onCheckedIn($event)
{
Log::debug('onCheckedIn in the Checkoutable listener fired');
@@ -145,61 +142,54 @@ class CheckoutableListener
return;
}
/**
* Send the appropriate notification
*/
if ($event->checkedOutTo && $event->checkoutable){
$acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
->where('assigned_to_id', $event->checkedOutTo->id)
->get();
$shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
$shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
$shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
foreach($acceptances as $acceptance){
if($acceptance->isPending()){
$acceptance->delete();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return;
}
if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
/**
* Send the appropriate notification
*/
if ($event->checkedOutTo && $event->checkoutable) {
$acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
->where('assigned_to_id', $event->checkedOutTo->id)
->get();
foreach ($acceptances as $acceptance) {
if ($acceptance->isPending()) {
$acceptance->delete();
}
}
}
}
$settings = Setting::getSettings();
$adminCcEmailsArray = [];
if($settings->admin_cc_email !== '') {
$adminCcEmail = $settings->admin_cc_email;
$adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail));
}
$ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckinMailType($event);
$notifiable = $this->getNotifiableUsers($event);
$mailable = $this->getCheckinMailType($event);
$notifiable = $this->getNotifiableUser($event);
// Send email notifications
try {
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
* 4. If the admin CC email is set, even if the item being checked in doesn't have an email address (location, etc)
*/
$notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
// Send a checkout email to the admin's CC addresses, even if the target has no email
if (!empty($ccEmails)) {
Mail::to($ccEmails)->send($mailable);
Log::info('Checkin Mail sent to CC addresses');
$shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
[$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
try {
if (!empty($to)) {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
Log::info('Checkin Mail sent to CC addresses');
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
// Send a checkout email to the target if it has an email
if (!empty($notifiable->email)) {
Mail::to($notifiable)->send($mailable);
Log::info('Checkin Mail sent to checkout target');
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
// Send Webhook notification
try {
if ($this->shouldSendWebhookNotification()) {
if ($shouldSendWebhookNotification) {
// Send Webhook notification
try {
if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckinNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
@@ -208,25 +198,24 @@ class CheckoutableListener
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event));
}
}
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') );
}
else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
} else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail'));
}
}
}
/**
* Generates a checkout acceptance
@@ -239,6 +228,7 @@ class CheckoutableListener
if ($checkedOutToType != "App\Models\User") {
return null;
}
if (!$event->checkoutable->requireAcceptance()) {
return null;
}
@@ -246,15 +236,22 @@ class CheckoutableListener
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$category = $this->getCategoryFromCheckoutable($event->checkoutable);
if ($category?->alert_on_response) {
$acceptance->alert_on_response_id = auth()->id();
}
$acceptance->save();
return $acceptance;
return $acceptance;
}
/**
* Get the appropriate notification for the event
*
* @param CheckoutableCheckedIn $event
*
* @param CheckoutableCheckedIn $event
* @return Notification
*/
private function getCheckinNotification($event)
@@ -268,7 +265,7 @@ class CheckoutableListener
break;
case Asset::class:
$notificationClass = CheckinAssetNotification::class;
break;
break;
case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class;
break;
@@ -276,9 +273,8 @@ class CheckoutableListener
Log::debug('Notification class: '.$notificationClass);
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
}
/**
* Get the appropriate notification for the event
*
@@ -320,6 +316,7 @@ class CheckoutableListener
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
}
private function getCheckinMailType($event){
$lookup = [
Accessory::class => CheckinAccessoryMail::class,
@@ -341,7 +338,8 @@ class CheckoutableListener
* @param $event
* @return mixed
*/
private function getNotifiableUsers($event){
private function getNotifiableUser($event)
{
// If it's assigned to an asset, get that asset's assignedTo object
if ($event->checkedOutTo instanceof Asset){
@@ -357,6 +355,7 @@ class CheckoutableListener
return $event->checkedOutTo;
}
}
private function webhookSelected(){
if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){
return 'slack';
@@ -365,60 +364,113 @@ class CheckoutableListener
return Setting::getSettings()->webhook_selected;
}
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
*/
public function subscribe($events)
{
$events->listen(
\App\Events\CheckoutableCheckedIn::class,
'App\Listeners\CheckoutableListener@onCheckedIn'
);
$events->listen(
\App\Events\CheckoutableCheckedOut::class,
'App\Listeners\CheckoutableListener@onCheckedOut'
);
}
private function shouldNotSendAnyNotifications($checkoutable): bool
{
if(in_array(get_class($checkoutable), $this->skipNotificationsFor)) {
return true;
}
//runs a check if the category wants to send checkin/checkout emails to users
$category = match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
default => null,
};
if (!$category->checkin_email) {
return true;
}
return false;
return in_array(get_class($checkoutable), $this->skipNotificationsFor);
}
private function shouldSendWebhookNotification(): bool
{
return Setting::getSettings() && Setting::getSettings()->webhook_endpoint;
}
private function checkoutableShouldSendEmail($event): bool
private function checkoutableCategoryShouldSendEmail(Model $checkoutable): bool
{
if($event->checkoutable instanceof LicenseSeat){
return $event->checkoutable->license->checkin_email();
if ($checkoutable instanceof LicenseSeat) {
return $checkoutable->license->checkin_email();
}
return (method_exists($event->checkoutable, 'checkin_email') && $event->checkoutable->checkin_email());
return (method_exists($checkoutable, 'checkin_email') && $checkoutable->checkin_email());
}
private function newMicrosoftTeamsWebhookEnabled(): bool
{
return Setting::getSettings()->webhook_selected === 'microsoft' && Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows');
}
private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool
{
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
*/
if ($checkoutable->requireAcceptance()) {
return true;
}
if ($checkoutable->getEula()) {
return true;
}
if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) {
return true;
}
return false;
}
private function shouldSendEmailToAlertAddress($acceptance = null): bool
{
$setting = Setting::getSettings();
if (!$setting) {
return false;
}
if (is_null($acceptance) && !$setting->admin_cc_always) {
return false;
}
return (bool) $setting->admin_cc_email;
}
private function getFormattedAlertAddresses(): array
{
$alertAddresses = Setting::getSettings()->admin_cc_email;
if ($alertAddresses !== '') {
return array_filter(array_map('trim', explode(',', $alertAddresses)));
}
return [];
}
private function generateEmailRecipients(
bool $shouldSendEmailToUser,
bool $shouldSendEmailToAlertAddress,
mixed $notifiable
): array {
$to = [];
$cc = [];
// if user && cc: to user, cc admin
if ($shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
$to[] = $notifiable;
$cc[] = $this->getFormattedAlertAddresses();
}
// if user && no cc: to user
if ($shouldSendEmailToUser && !$shouldSendEmailToAlertAddress) {
$to[] = $notifiable;
}
// if no user && cc: to admin
if (!$shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
$to[] = $this->getFormattedAlertAddresses();
}
return array($to, $cc);
}
private function getCategoryFromCheckoutable(Model $checkoutable): ?Category
{
return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
};
}
}

View File

@@ -6,6 +6,8 @@ use Livewire\Component;
class CategoryEditForm extends Component
{
public bool $alertOnResponse;
public $defaultEulaText;
public $eulaText;

View File

@@ -35,10 +35,14 @@ class Importer extends Component
public $accessories_fields;
public $assets_fields;
public $users_fields;
public $assetmodels_fields;
public $suppliers_fields;
public $licenses_fields;
public $locations_fields;
public $consumables_fields;
public $components_fields;
public $manufacturers_fields;
public $categories_fields;
public $aliases_fields;
protected $rules = [
@@ -85,9 +89,6 @@ class Importer extends Component
case 'component':
$results = $this->components_fields;
break;
case 'consumable':
$results = $this->consumables_fields;
break;
case 'license':
$results = $this->licenses_fields;
break;
@@ -97,8 +98,14 @@ class Importer extends Component
case 'location':
$results = $this->locations_fields;
break;
case 'user':
$results = $this->users_fields;
case 'supplier':
$results = $this->suppliers_fields;
break;
case 'manufacturer':
$results = $this->manufacturers_fields;
break;
case 'category':
$results = $this->categories_fields;
break;
default:
$results = [];
@@ -128,7 +135,7 @@ class Importer extends Component
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
//no, this key is *INVALID* for this import type. Better set it to null
//no, this key is *INVALID* for this import type. Better set it to null,
// and we'll hope that the $aliases_fields or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think.
@@ -149,7 +156,7 @@ class Importer extends Component
// in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$type])) {
$this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
continue 3; // bust out of both of these loops and the surrounding one - e.g. move on to the next header
}
}
}
@@ -171,6 +178,9 @@ class Importer extends Component
'license' => trans('general.licenses'),
'location' => trans('general.locations'),
'user' => trans('general.users'),
'supplier' => trans('general.suppliers'),
'manufacturer' => trans('general.manufacturers'),
'category' => trans('general.categories'),
];
/**
@@ -193,6 +203,7 @@ class Importer extends Component
];
$this->assets_fields = [
'id' => trans('general.id'),
'asset_eol_date' => trans('admin/hardware/form.eol_date'),
'asset_model' => trans('general.model_name'),
'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]),
@@ -319,6 +330,8 @@ class Importer extends Component
'location' => trans('general.location'),
'manager_first_name' => trans('general.importer.manager_first_name'),
'manager_last_name' => trans('general.importer.manager_last_name'),
'manager_employee_num' => trans('general.importer.manager_employee_num'),
'manager_username' => trans('general.importer.manager_username'),
'notes' => trans('general.notes'),
'phone_number' => trans('admin/users/table.phone'),
'remote' => trans('admin/users/general.remote'),
@@ -332,6 +345,7 @@ class Importer extends Component
$this->locations_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
'city' => trans('general.city'),
@@ -340,13 +354,52 @@ class Importer extends Component
'ldap_ou' => trans('admin/locations/table.ldap_ou'),
'manager' => trans('general.importer.manager_full_name'),
'manager_username' => trans('general.importer.manager_username'),
'name' => trans('general.item_name_var', ['item' => trans('general.location')]),
'notes' => trans('general.notes'),
'parent_location' => trans('admin/locations/table.parent'),
'state' => trans('general.state'),
'zip' => trans('general.zip'),
];
$this->suppliers_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
'city' => trans('general.city'),
'notes' => trans('general.notes'),
'state' => trans('general.state'),
'zip' => trans('general.zip'),
'phone' => trans('general.phone'),
'fax' => trans('general.fax'),
'url' => trans('general.url'),
'contact' => trans('general.contact'),
'email' => trans('general.email'),
];
$this->manufacturers_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'notes' => trans('general.notes'),
'support_phone' => trans('admin/manufacturers/table.support_phone'),
'support_url' => trans('admin/manufacturers/table.support_url'),
'support_email' => trans('admin/manufacturers/table.support_email'),
'warranty_lookup_url' => trans('admin/manufacturers/table.warranty_lookup_url'),
'url' => trans('general.url'),
];
$this->categories_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'notes' => trans('general.notes'),
'category_type' => trans('admin/categories/general.import_category_type'),
'eula_text' => trans('admin/categories/general.import_eula_text'),
'use_default_eula' => trans('admin/categories/general.use_default_eula_column'),
'require_acceptance' => trans('admin/categories/general.import_require_acceptance'),
'checkin_email' => trans('admin/categories/general.import_checkin_email'),
];
$this->assetmodels_fields = [
'category' => trans('general.category'),
'eol' => trans('general.eol'),
@@ -371,6 +424,8 @@ class Importer extends Component
'consumable name',
'component name',
'name',
'supplier name',
'location name',
],
'item_no' => [
'item number',

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Livewire;
use App\Helpers\Helper;
use App\Models\Setting;
use Livewire\Component;
class LocationScopeCheck extends Component
{
public $mismatched = [];
public $setting;
public $is_tested = false;
public function check_locations()
{
$this->mismatched = Helper::test_locations_fmcs(false);
$this->is_tested = true;
}
public function mount() {
$this->setting = Setting::getSettings();
}
public function render()
{
return view('livewire.location-scope-check');
}
}

View File

@@ -71,12 +71,12 @@ class SlackSettingsForm extends Component
$this->setting = Setting::getSettings();
$this->save_button = trans('general.save');
$this->webhook_selected = $this->setting->webhook_selected;
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"];
$this->webhook_selected = ($this->setting->webhook_selected !== '') ? $this->setting->webhook_selected : 'slack';
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"] ?? $this->webhook_text['slack']["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"] ?? $this->webhook_text['slack']["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"] ?? $this->webhook_text['slack']["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"] ?? $this->webhook_text['slack']["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"] ?? $this->webhook_text['slack']["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname;
@@ -90,7 +90,7 @@ class SlackSettingsForm extends Component
$this->isDisabled= '';
}
if($this->webhook_selected === 'microsoft' && $this->teams_webhook_deprecated) {
session()->flash('warning', 'The selected Microsoft Teams webhook URL will be deprecated Jan 31st, 2025. Please use a workflow URL. Microsofts Documentation on creating a workflow can be found <a href="https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498" target="_blank"> here.</a>');
session()->flash('warning', trans('admin/settings/message.webhook.ms_teams_deprecation'));
}
}
public function updated($field) {

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\CheckoutAcceptance;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckoutAcceptanceResponseMail extends Mailable
{
use Queueable, SerializesModels;
public CheckoutAcceptance $acceptance;
public User $recipient;
public bool $wasAccepted;
/**
* Create a new message instance.
*/
public function __construct(CheckoutAcceptance $acceptance, User $recipient, bool $wasAccepted)
{
$this->acceptance = $acceptance;
$this->recipient = $recipient;
$this->wasAccepted = $wasAccepted;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$subject = $this->wasAccepted
? trans('mail.initiated_accepted')
: trans('mail.initiated_declined');
return new Envelope(
subject: $subject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'mail.markdown.checkout-acceptance-response',
with: [
'assignedTo' => $this->acceptance->assignedTo,
'introduction' => $this->introduction(),
'item' => $this->acceptance->checkoutable,
'note' => $this->acceptance->note,
'recipient' => $this->recipient,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
private function introduction(): string
{
return $this->wasAccepted
? trans('mail.following_accepted')
: trans('mail.following_declined');
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -14,7 +15,7 @@ use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
* @version v1.0
*/
class Accessory extends SnipeModel
{
@@ -22,6 +23,7 @@ class Accessory extends SnipeModel
protected $presenter = \App\Presenters\AccessoryPresenter::class;
use CompanyableTrait;
use HasUploads;
use Loggable, Presentable;
use SoftDeletes;
@@ -54,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',
@@ -69,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;
@@ -102,29 +104,11 @@ class Accessory extends SnipeModel
];
/**
* Establishes the accessories -> action logs -> uploads relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.1.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
->where('item_type', '=', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/**
* 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()
@@ -137,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)
@@ -152,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()
@@ -164,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()
@@ -176,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()
@@ -188,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()
@@ -217,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()
*/
@@ -235,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()
@@ -251,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()
@@ -264,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()
@@ -276,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()
@@ -290,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()
@@ -303,12 +287,12 @@ 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()
{
return $this->category->checkin_email;
return $this->category?->checkin_email;
}
/**
@@ -316,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()
@@ -329,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()
@@ -349,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()
@@ -366,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()
@@ -381,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)
{
@@ -408,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)
@@ -426,7 +410,6 @@ class Accessory extends SnipeModel
/**
* Query builder scope to order on created_by name
*
*/
public function scopeOrderByCreatedByName($query, $order)
{
@@ -434,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

@@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* 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
{
@@ -69,8 +69,8 @@ class Actionlog extends SnipeModel
*/
protected $searchableRelations = [
'company' => ['name'],
'adminuser' => ['first_name','last_name','username', 'email'],
'user' => ['first_name','last_name','username', 'email'],
'adminuser' => ['first_name','last_name','username', 'email', 'employee_num'],
'user' => ['first_name','last_name','username', 'email', 'employee_num'],
'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'],
@@ -96,24 +96,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();
}
}
});
);
}
@@ -121,7 +128,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()
@@ -133,7 +140,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()
@@ -146,7 +153,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()
@@ -158,7 +165,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()
@@ -170,7 +177,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()
@@ -182,7 +189,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()
@@ -194,7 +201,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()
@@ -206,7 +213,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()
@@ -222,7 +229,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()
@@ -239,21 +246,21 @@ 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()
{
return $this->morphTo('item')
->where('action_type', '=', 'uploaded')
->withTrashed();
->where('action_type', '=', 'uploaded')
->withTrashed();
}
/**
* 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()
@@ -265,20 +272,20 @@ 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()
{
return $this->belongsTo(User::class, 'created_by')
->withTrashed();
->withTrashed();
}
/**
* 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()
@@ -291,7 +298,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()
@@ -303,7 +310,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()
@@ -316,7 +323,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')
@@ -334,7 +341,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)
@@ -355,7 +362,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)
@@ -376,7 +383,7 @@ class Actionlog extends SnipeModel
if ($this->created_at > $override_default_next) {
$next_audit_days = '-'.$next_audit_days;
}
return $next_audit_days;
}
@@ -384,7 +391,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)
@@ -401,24 +408,24 @@ 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()
{
return $this->all()
->where('action_type', '!=', 'uploaded')
->orderBy('item_id', 'asc')
->orderBy('created_at', 'asc')
->get();
->where('action_type', '!=', 'uploaded')
->orderBy('item_id', 'asc')
->orderBy('created_at', 'asc')
->get();
}
/**
* 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
@@ -430,11 +437,12 @@ 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';
}
// This is probably NOT an API call
// This is probably NOT an API call
if (request()->filled('_token')) {
return 'gui';
}
@@ -444,6 +452,62 @@ 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;
}
}
public function uploads_file_path()
{
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;
}
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{
@@ -454,4 +518,4 @@ class Actionlog extends SnipeModel
{
return $query->leftJoin('users as admin_sort', 'action_logs.created_by', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ use Watson\Validating\ValidatingTrait;
/**
* Model for Asset Maintenances.
*
* @version v1.0
* @version v1.0
*/
class AssetMaintenance extends Model implements ICompanyableChild
{
@@ -26,12 +26,12 @@ class AssetMaintenance extends Model implements ICompanyableChild
protected $table = 'asset_maintenances';
protected $rules = [
'asset_id' => 'required|integer',
'supplier_id' => 'required|integer',
'supplier_id' => 'nullable|integer',
'asset_maintenance_type' => 'required',
'title' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable',
'completion_date' => 'date_format:Y-m-d|nullable|after_or_equal:start_date',
'notes' => 'string|nullable',
'cost' => 'numeric|nullable',
];
@@ -93,7 +93,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* getImprovementOptions
*
* @return array
* @return array
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
*/
@@ -157,20 +157,21 @@ class AssetMaintenance extends Model implements ICompanyableChild
* asset
* Get asset for this improvement
*
* @return mixed
* @return mixed
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
*/
public function asset()
{
return $this->belongsTo(\App\Models\Asset::class, 'asset_id')
->withTrashed();
->withTrashed();
}
/**
* Get the admin who created the maintenance
*
* @return mixed
* @return mixed
* @author A. Gianotto <snipe@snipe.net>
* @version v3.0
*/
@@ -183,7 +184,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
public function supplier()
{
return $this->belongsTo(\App\Models\Supplier::class, 'supplier_id')
->withTrashed();
->withTrashed();
}
/**
@@ -195,8 +196,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* Query builder scope to order on a supplier
*
* @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
*/
@@ -211,8 +212,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* Query builder scope to order on asset tag
*
* @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
*/
@@ -225,8 +226,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* Query builder scope to order on asset tag
*
* @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
*/
@@ -239,8 +240,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* Query builder scope to order on serial
*
* @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
*/
@@ -253,8 +254,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/**
* Query builder scope to order on status label 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
*/
@@ -265,6 +266,21 @@ class AssetMaintenance extends Model implements ICompanyableChild
->orderBy('maintained_asset_status.name', $order);
}
/**
* Query builder scope to order on status label name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderLocationName($query, $order)
{
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id')
->orderBy('maintained_asset_location.name', $order);
}
/**
* Query builder scope to order on the user that created it
*/

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -16,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
{
@@ -24,6 +25,7 @@ class AssetModel extends SnipeModel
use SoftDeletes;
use Loggable, Requestable, Presentable;
use TwoColumnUniqueUndeletedTrait;
use HasUploads;
/**
* Whether the model should inject its identifier to the unique
@@ -103,7 +105,7 @@ class AssetModel extends SnipeModel
* 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()
@@ -115,7 +117,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()
@@ -127,7 +129,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()
@@ -139,7 +141,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()
@@ -151,7 +153,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()
@@ -161,14 +163,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()
@@ -182,7 +184,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()
@@ -199,7 +201,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()
@@ -209,27 +211,12 @@ class AssetModel extends SnipeModel
&& ($this->deleted_at == '');
}
/**
* Get uploads for this model
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany('\App\Models\Actionlog', 'item_id')
->where('item_type', '=', AssetModel::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/**
* 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()
@@ -248,10 +235,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
*/
@@ -264,9 +251,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
*/
@@ -278,8 +265,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
*/
@@ -287,23 +274,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
*/
@@ -315,8 +310,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
*/
@@ -332,7 +327,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
{
@@ -32,6 +32,7 @@ class Category extends SnipeModel
protected $hidden = ['created_by', 'deleted_at'];
protected $casts = [
'alert_on_response' => 'boolean',
'created_by' => 'integer',
];
@@ -69,6 +70,7 @@ class Category extends SnipeModel
'eula_text',
'name',
'require_acceptance',
'alert_on_response',
'use_default_eula',
'created_by',
'notes',
@@ -94,12 +96,20 @@ 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()
{
// We have to check for models as well if the category type is asset
if ($this->category_type == 'asset') {
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->models_count == 0)
&& ($this->deleted_at == '');
}
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->deleted_at == '');
@@ -109,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()
@@ -121,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()
@@ -133,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()
@@ -145,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()
@@ -160,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()
@@ -171,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;
}
}
@@ -191,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()
@@ -208,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()
@@ -221,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()
@@ -239,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()
@@ -266,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)
@@ -283,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

@@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model
protected $casts = [
'accepted_at' => 'datetime',
'declined_at' => 'datetime',
'alert_on_response_id' => 'integer',
];
/**
@@ -65,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)
@@ -78,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)
{
@@ -98,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)
{
@@ -115,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)
@@ -126,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

@@ -13,11 +13,13 @@ use Illuminate\Support\Facades\Schema;
/**
* Model for Companies.
*
* @version v1.8
* @version v1.8
*/
final class Company extends SnipeModel
{
use HasFactory;
use CompanyableTrait;
protected $table = 'companies';
@@ -26,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;
@@ -100,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)
@@ -127,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)
@@ -146,10 +148,10 @@ final class Company extends SnipeModel
if (!is_string($companyable)) {
$company_table = $companyable->getModel()->getTable();
try {
// This is primary for the gate:allows-check in location->isDeletable()
// This is primarily for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before()
if (!$companyable instanceof Company && !Schema::hasColumn($company_table, 'company_id')) {
if (!Schema::hasColumn($company_table, 'company_id')) {
return true;
}
@@ -160,12 +162,19 @@ final class Company extends SnipeModel
if (auth()->user()) {
Log::warning('Companyable is '.$companyable);
// Log::warning('Companyable is '.$companyable);
$current_user_company_id = auth()->user()->company_id;
$companyable_company_id = $companyable->company_id;
return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || auth()->user()->isSuperUser();
// Set this to check companyable on company
if ($companyable instanceof Company) {
$companyable_company_id = $companyable->id;
}
return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
}
return false;
}
public static function isCurrentUserAuthorized()
@@ -183,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()
@@ -200,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)
@@ -256,14 +265,14 @@ 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)
{
// If not logged in and hitting this, assume we are on the command line and don't scope?'
// If not logged in and hitting this, assume we are on the command line and don't scope?
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && auth()->user()->isSuperUser()) || (! Auth::hasUser())) {
return $query;
} else {
@@ -280,11 +289,16 @@ final class Company extends SnipeModel
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
$company_id = null;
// Get the company ID of the logged-in user, or set it to null if there is no company associated with the user
if (Auth::hasUser()) {
$company_id = auth()->user()->company_id;
} else {
$company_id = null;
}
// If we are scoping the companies table itself, look for the company.id
if ($query->getModel()->getTable() == 'companies') {
return $query->where('companies.id', '=', $company_id);
}
@@ -297,6 +311,8 @@ final class Company extends SnipeModel
return $query->where($table.$column, '=', $company_id);
}
}
public function adminuser()
@@ -311,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)
@@ -324,17 +340,18 @@ final class Company extends SnipeModel
return $query;
} else {
$f = function ($q) {
Log::debug('scopeCompanyablesDirectly firing ');
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)

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