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 200 additions and 48 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,
],
[

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,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());
}
}