Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
This commit is contained in:
snipe 2024-05-08 12:16:17 +01:00
commit c8fbf7640c
13 changed files with 197 additions and 45 deletions

View file

@ -75,8 +75,8 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',)
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count');
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy')
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
if ($request->filled('activated')) {
@ -187,6 +187,14 @@ class UsersController extends Controller
$users->has('accessories', '=', $request->input('accessories_count'));
}
if ($request->filled('manages_users_count')) {
$users->has('manages_users_count', '=', $request->input('manages_users_count'));
}
if ($request->filled('manages_locations_count')) {
$users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
}
if ($request->filled('autoassign_licenses')) {
$users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
}
@ -244,6 +252,8 @@ class UsersController extends Controller
'licenses_count',
'consumables_count',
'accessories_count',
'manages_user_count',
'manages_locations_count',
'phone',
'address',
'city',
@ -405,11 +415,15 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
$user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count')->findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
$user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
if ($user = Company::scopeCompanyables($user)->find($id)) {
$this->authorize('view', $user);
return (new UsersTransformer)->transformUser($user);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
return (new UsersTransformer)->transformUser($user);
}
@ -470,7 +484,6 @@ class UsersController extends Controller
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
@ -480,12 +493,6 @@ class UsersController extends Controller
if ($user->save()) {
// Sync group memberships:
// This was changed in Snipe-IT v4.6.x to 4.7, since we upgraded to Laravel 5.5
// which changes the behavior of has vs filled.
// The $request->has method will now return true even if the input value is an empty string or null.
// A new $request->filled method has was added that provides the previous behavior of the has method.
// Check if the request has groups passed and has a value
if ($request->filled('groups')) {

View file

@ -21,25 +21,36 @@ class AssetCountForSidebar
/**
* This needs to be set for the /setup process, since the tables might not exist yet
*/
$total_assets = 0;
$total_due_for_checkin = 0;
$total_overdue_for_checkin = 0;
$total_due_for_audit = 0;
$total_overdue_for_audit = 0;
try {
$total_rtd_sidebar = Asset::RTD()->count();
view()->share('total_rtd_sidebar', $total_rtd_sidebar);
$settings = Setting::getSettings();
view()->share('settings', $settings);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_assets = Asset::RTD()->count();
$total_assets = Asset::all()->count();
if ($settings->show_archived_in_list != '1') {
$total_assets -= Asset::Archived()->count();
}
view()->share('total_assets', $total_assets);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_rtd_sidebar = Asset::RTD()->count();
view()->share('total_rtd_sidebar', $total_rtd_sidebar);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_deployed_sidebar = Asset::Deployed()->count();
view()->share('total_deployed_sidebar', $total_deployed_sidebar);
@ -75,13 +86,6 @@ class AssetCountForSidebar
\Log::debug($e);
}
try {
$settings = Setting::getSettings();
view()->share('settings', $settings);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_due_for_audit = Asset::DueForAudit($settings)->count();
view()->share('total_due_for_audit', $total_due_for_audit);

View file

@ -21,6 +21,7 @@ class UsersTransformer
public function transformUser(User $user)
{
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar),
@ -64,6 +65,8 @@ class UsersTransformer
'licenses_count' => (int) $user->licenses_count,
'accessories_count' => (int) $user->accessories_count,
'consumables_count' => (int) $user->consumables_count,
'manages_users_count' => (int) $user->manages_users_count,
'manages_locations_count' => (int) $user->manages_locations_count,
'company' => ($user->company) ? ['id' => (int) $user->company->id, 'name'=> e($user->company->name)] : null,
'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id,

View file

@ -214,10 +214,12 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
public function isDeletable()
{
return Gate::allows('delete', $this)
&& ($this->assets()->count() === 0)
&& ($this->licenses()->count() === 0)
&& ($this->consumables()->count() === 0)
&& ($this->accessories()->count() === 0)
&& ($this->assets->count() === 0)
&& ($this->licenses->count() === 0)
&& ($this->consumables->count() === 0)
&& ($this->accessories->count() === 0)
&& ($this->managedLocations->count() === 0)
&& ($this->managesUsers->count() === 0)
&& ($this->deleted_at == '');
}
@ -410,6 +412,19 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->belongsTo(self::class, 'manager_id')->withTrashed();
}
/**
* Establishes the user -> managed users relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.4.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function managesUsers()
{
return $this->hasMany(\App\Models\User::class, 'manager_id');
}
/**
* Establishes the user -> managed locations relationship
*

View file

@ -221,7 +221,7 @@ class UserPresenter extends Presenter
'switchable' => true,
'escape' => true,
'class' => 'css-barcode',
'title' => 'Assets',
'title' => trans('general.assets'),
'visible' => true,
],
[
@ -230,7 +230,7 @@ class UserPresenter extends Presenter
'sortable' => true,
'switchable' => true,
'class' => 'css-license',
'title' => 'License',
'title' => trans('general.licenses'),
'visible' => true,
],
[
@ -239,7 +239,7 @@ class UserPresenter extends Presenter
'sortable' => true,
'switchable' => true,
'class' => 'css-consumable',
'title' => 'Consumables',
'title' => trans('general.consumables'),
'visible' => true,
],
[
@ -248,7 +248,25 @@ class UserPresenter extends Presenter
'sortable' => true,
'switchable' => true,
'class' => 'css-accessory',
'title' => 'Accessories',
'title' => trans('general.accessories'),
'visible' => true,
],
[
'field' => 'manages_users_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'class' => 'css-users',
'title' => trans('admin/users/table.managed_users'),
'visible' => true,
],
[
'field' => 'manages_locations_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'class' => 'css-location',
'title' => trans('admin/users/table.managed_locations'),
'visible' => true,
],
[

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,8 @@
{
"/js/build/app.js": "/js/build/app.js?id=6bbc4dd6b643fefe492261fdfe6fae5a",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/build/overrides.css": "/css/build/overrides.css?id=496c37399034d10dcc70be93f0a652ee",
"/css/build/app.css": "/css/build/app.css?id=6fe97f315d9bddfbcb4b00e4fb5ad91e",
"/css/build/overrides.css": "/css/build/overrides.css?id=c071d0741611371e84c380de2fb13b46",
"/css/build/app.css": "/css/build/app.css?id=cd07d2f45297c702527a330dce45dd46",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f25c77ed07053646a42e9c19923d24fa",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=03075904b967308132b810bc0205ab1c",
@ -18,7 +18,7 @@
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/all.css": "/css/dist/all.css?id=b9fabcaa30b6364ecc464012fc124cb0",
"/css/dist/all.css": "/css/dist/all.css?id=3785387b68b5d74dccd037fe5340e038",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=0141634c24336be626e05c8b77d1fa27",

View file

@ -586,6 +586,8 @@ th.css-barcode > .th-inner,
th.css-license > .th-inner,
th.css-consumable > .th-inner,
th.css-envelope > .th-inner,
th.css-users > .th-inner,
th.css-location > .th-inner,
th.css-accessory > .th-inner
{
font-size: 0px;
@ -602,6 +604,8 @@ th.css-barcode > .th-inner::before,
th.css-license > .th-inner::before,
th.css-consumable > .th-inner::before,
th.css-envelope > .th-inner::before,
th.css-users > .th-inner::before,
th.css-location > .th-inner::before,
th.css-accessory > .th-inner::before
{
@ -621,6 +625,7 @@ th.css-padlock > .th-inner::before
}
/**
BEGIN ICON TABLE HEADERS
Set the font-weight css property as 900 (For Solid), 400 (Regular or Brands), 300 (Light for pro icons).
**/
th.css-barcode > .th-inner::before
@ -643,12 +648,20 @@ th.css-envelope > .th-inner::before
content: "\f0e0"; font-family: "Font Awesome 5 Free"; font-weight: 400;
}
th.css-accessory > .th-inner::before
{
content: "\f11c"; font-family: "Font Awesome 5 Free"; font-weight: 400;
}
th.css-users > .th-inner::before {
content: "\f0c0"; font-family: "Font Awesome 5 Free"; font-size: 15px;
}
th.css-location > .th-inner::before {
content: "\f3c5"; font-family: "Font Awesome 5 Free"; font-size: 19px; margin-bottom: 0px;
}
.small-box .inner {
padding-left: 15px;
padding-right: 15px;

View file

@ -20,6 +20,7 @@ return array(
'lock_passwords' => 'Login details cannot be changed on this installation.',
'manager' => 'Manager',
'managed_locations' => 'Managed Locations',
'managed_users' => 'Managed Users',
'name' => 'Name',
'nogroup' => 'No groups have been created yet. To add one, visit: ',
'notes' => 'Notes',

View file

@ -89,9 +89,9 @@
</a>
</li>
@if ($user->managedLocations()->count() >= 0 )
@if ($user->managedLocations->count() >= 0 )
<li>
<a href="#managed" data-toggle="tab">
<a href="#managed-locations" data-toggle="tab">
<span class="hidden-lg hidden-md">
<i class="fas fa-map-marker-alt fa-2x"></i></span>
<span class="hidden-xs hidden-sm">{{ trans('admin/users/table.managed_locations') }}
@ -100,7 +100,19 @@
</li>
@endif
@can('update', $user)
@if ($user->managesUsers->count() >= 0 )
<li>
<a href="#managed-users" data-toggle="tab">
<span class="hidden-lg hidden-md">
<i class="fa-solid fa-users fa-2x"></i></span>
<span class="hidden-xs hidden-sm">{{ trans('admin/users/table.managed_users') }}
{!! ($user->managesUsers->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($user->managesUsers->count()).'</badge>' : '' !!}
</a>
</li>
@endif
@can('update', $user)
<li class="dropdown pull-right">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="hidden-xs"><i class="fas fa-cog" aria-hidden="true"></i></span>
@ -114,7 +126,7 @@
<ul class="dropdown-menu">
<li><a href="{{ route('users.edit', $user->id) }}">{{ trans('admin/users/general.edit') }}</a></li>
<li><a href="{{ route('users.clone.show', $user->id) }}">{{ trans('admin/users/general.clone') }}</a></li>
@if ((Auth::user()->id !== $user->id) && (!config('app.lock_passwords')) && ($user->deleted_at==''))
@if ((Auth::user()->id !== $user->id) && (!config('app.lock_passwords')) && ($user->deleted_at=='') && ($user->isDeletable()))
<li><a href="{{ route('users.destroy', $user->id) }}">{{ trans('button.delete') }}</a></li>
@endif
</ul>
@ -221,11 +233,15 @@
@can('delete', $user)
@if ($user->deleted_at=='')
<div class="col-md-12" style="padding-top: 30px;">
<form action="{{route('users.destroy',$user->id)}}" method="POST">
{{csrf_field()}}
{{ method_field("DELETE")}}
<button style="width: 100%;" class="btn btn-sm btn-warning hidden-print">{{ trans('button.delete')}}</button>
</form>
@if ($user->isDeletable())
<form action="{{route('users.destroy',$user->id)}}" method="POST">
{{csrf_field()}}
{{ method_field("DELETE")}}
<button style="width: 100%;" class="btn btn-sm btn-warning hidden-print">{{ trans('button.delete')}}</button>
</form>
@else
<button style="width: 100%;" class="btn btn-sm btn-warning hidden-print disabled">{{ trans('button.delete')}}</button>
@endif
</div>
<div class="col-md-12" style="padding-top: 5px;">
<form action="{{ route('users/bulkedit') }}" method="POST">
@ -1014,7 +1030,7 @@
</div>
</div><!-- /.tab-pane -->
<div class="tab-pane" id="managed">
<div class="tab-pane" id="managed-locations">
@include('partials.locations-bulk-actions')
@ -1046,6 +1062,39 @@
</table>
</div>
<div class="tab-pane" id="managed-users">
@include('partials.locations-bulk-actions')
<table
data-columns="{{ \App\Presenters\UserPresenter::dataTableLayout() }}"
data-cookie-id-table="managedUsersTable"
data-click-to-select="true"
data-pagination="true"
data-id-table="managedUsersTable"
data-toolbar="#usersBulkEditToolbar"
data-bulk-button-id="#bulkUsersEditButton"
data-bulk-form-id="#usersBulkForm"
data-search="true"
data-show-footer="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
id="managedUsersTable"
class="table table-striped snipe-table"
data-url="{{ route('api.users.index', ['manager_id' => $user->id]) }}"
data-export-options='{
"fileName": "export-users-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div>
</div><!-- /consumables-tab -->
</div><!-- /.tab-content -->
</div><!-- nav-tabs-custom -->

View file

@ -0,0 +1,42 @@
<?php
namespace Tests\Feature\Api\Users;
use App\Models\Location;
use App\Models\User;
use Laravel\Passport\Passport;
use Tests\TestCase;
class UsersDeleteTest extends TestCase
{
public function testDisallowUserDeletionIfStillManagingPeople()
{
$manager = User::factory()->create(['first_name' => 'Manager', 'last_name' => 'McManagerson']);
User::factory()->create(['first_name' => 'Lowly', 'last_name' => 'Worker', 'manager_id' => $manager->id]);
$this->actingAs(User::factory()->deleteUsers()->create())->assertFalse($manager->isDeletable());
}
public function testDisallowUserDeletionIfStillManagingLocations()
{
$manager = User::factory()->create(['first_name' => 'Manager', 'last_name' => 'McManagerson']);
Location::factory()->create(['manager_id' => $manager->id]);
$this->actingAs(User::factory()->deleteUsers()->create())->assertFalse($manager->isDeletable());
}
public function testAllowUserDeletionIfNotManagingLocations()
{
$manager = User::factory()->create(['first_name' => 'Manager', 'last_name' => 'McManagerson']);
$this->actingAs(User::factory()->deleteUsers()->create())->assertTrue($manager->isDeletable());
}
public function testDisallowUserDeletionIfNoDeletePermissions()
{
$manager = User::factory()->create(['first_name' => 'Manager', 'last_name' => 'McManagerson']);
Location::factory()->create(['manager_id' => $manager->id]);
$this->actingAs(User::factory()->editUsers()->create())->assertFalse($manager->isDeletable());
}
}