mirror of
https://github.com/snipe/snipe-it.git
synced 2025-03-05 20:52:15 -08:00
Merge pull request #16285 from marcusmoore/bug/sc-28148
Re-added ability to add notes to assets
This commit is contained in:
commit
b0fa059a28
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class NoteAdded
|
|
||||||
{
|
|
||||||
use Dispatchable, SerializesModels;
|
|
||||||
public $itemNoteAddedOn;
|
|
||||||
public $note;
|
|
||||||
public $noteAddedBy;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($itemNoteAddedOn, User $noteAddedBy, $note)
|
|
||||||
{
|
|
||||||
$this->itemNoteAddedOn = $itemNoteAddedOn;
|
|
||||||
$this->note = $note;
|
|
||||||
$this->noteAddedBy = $noteAddedBy;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Events\NoteAdded;
|
|
||||||
use App\Helpers\Helper;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class NotesController extends Controller
|
|
||||||
{
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'note' => 'required|string|max:500',
|
|
||||||
'type' => [
|
|
||||||
'required',
|
|
||||||
Rule::in(['asset']),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// This can be made dynamic by using $request->input('type') to determine which model type to add the note to.
|
|
||||||
// For now, we are only placing this on Assets
|
|
||||||
$item = Asset::findOrFail($request->input("id"));
|
|
||||||
$this->authorize('update', $item);
|
|
||||||
|
|
||||||
event(new NoteAdded($item, Auth::user(), $validated['note']));
|
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public function destroy(Request $request)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
42
app/Http/Controllers/NotesController.php
Normal file
42
app/Http/Controllers/NotesController.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Actionlog;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class NotesController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'id' => 'required',
|
||||||
|
'note' => 'required|string|max:500',
|
||||||
|
'type' => [
|
||||||
|
'required',
|
||||||
|
Rule::in(['asset']),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = Asset::findOrFail($validated['id']);
|
||||||
|
|
||||||
|
$this->authorize('update', $item);
|
||||||
|
|
||||||
|
$logaction = new Actionlog;
|
||||||
|
$logaction->item_id = $item->id;
|
||||||
|
$logaction->item_type = get_class($item);
|
||||||
|
$logaction->note = $validated['note'];
|
||||||
|
$logaction->created_by = Auth::id();
|
||||||
|
$logaction->logaction('note added');
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('hardware.show', $validated['id'])
|
||||||
|
->withFragment('history')
|
||||||
|
->with('success', trans('general.note_added'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ use App\Events\ItemAccepted;
|
||||||
use App\Events\ItemDeclined;
|
use App\Events\ItemDeclined;
|
||||||
use App\Events\LicenseCheckedIn;
|
use App\Events\LicenseCheckedIn;
|
||||||
use App\Events\LicenseCheckedOut;
|
use App\Events\LicenseCheckedOut;
|
||||||
use App\Events\NoteAdded;
|
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\LicenseSeat;
|
use App\Models\LicenseSeat;
|
||||||
|
@ -129,23 +128,6 @@ class LogListener
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note is added to action log
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public function onNoteAdded(NoteAdded $event)
|
|
||||||
{
|
|
||||||
$logaction = new Actionlog();
|
|
||||||
$logaction->item_id = $event->itemNoteAddedOn->id;
|
|
||||||
$logaction->item_type = get_class($event->itemNoteAddedOn);
|
|
||||||
$logaction->note = $event->note; //this is the received alphanumeric text from the box
|
|
||||||
$logaction->created_by = $event->noteAddedBy->id;
|
|
||||||
$logaction->action_type = 'note_added';
|
|
||||||
$logaction->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the listeners for the subscriber.
|
* Register the listeners for the subscriber.
|
||||||
*
|
*
|
||||||
|
|
|
@ -28,7 +28,7 @@ $(function () {
|
||||||
|
|
||||||
var baseUrl = $('meta[name="baseUrl"]').attr('content');
|
var baseUrl = $('meta[name="baseUrl"]').attr('content');
|
||||||
//handle modal-add-interstitial calls
|
//handle modal-add-interstitial calls
|
||||||
var model, select, refreshSelector, hasnopayload;
|
var model, select, refreshSelector;
|
||||||
|
|
||||||
if($('#createModal').length == 0) {
|
if($('#createModal').length == 0) {
|
||||||
$('body').append('<div class="modal fade" id="createModal"></div><!-- /.modal -->');
|
$('body').append('<div class="modal fade" id="createModal"></div><!-- /.modal -->');
|
||||||
|
@ -40,8 +40,6 @@ $(function () {
|
||||||
select = link.data("select");
|
select = link.data("select");
|
||||||
refreshSelector = link.data("refresh");
|
refreshSelector = link.data("refresh");
|
||||||
|
|
||||||
hasnopayload = link.data("hasnopayload");
|
|
||||||
|
|
||||||
$('#createModal').load(link.attr('href'),function () {
|
$('#createModal').load(link.attr('href'),function () {
|
||||||
|
|
||||||
// this sets the focus to be the name field
|
// this sets the focus to be the name field
|
||||||
|
@ -123,13 +121,12 @@ $(function () {
|
||||||
$('#modal_error_msg').html(error_message).show();
|
$('#modal_error_msg').html(error_message).show();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(!hasnopayload) {
|
|
||||||
var id = result.payload.id;
|
var id = result.payload.id;
|
||||||
var name = result.payload.name || (result.payload.first_name + " " + result.payload.last_name);
|
var name = result.payload.name || (result.payload.first_name + " " + result.payload.last_name);
|
||||||
if (!id || !name) {
|
if (!id || !name) {
|
||||||
console.error("Could not find resulting name or ID from modal-create. Name: " + name + ", id: " + id);
|
console.error("Could not find resulting name or ID from modal-create. Name: " + name + ", id: " + id);
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$('#createModal').modal('hide');
|
$('#createModal').modal('hide');
|
||||||
$('#createModal').html("");
|
$('#createModal').html("");
|
||||||
|
|
|
@ -217,14 +217,13 @@
|
||||||
|
|
||||||
<!-- Add notes -->
|
<!-- Add notes -->
|
||||||
@can('update', \App\Models\Asset::class)
|
@can('update', \App\Models\Asset::class)
|
||||||
<!--
|
|
||||||
|
|
||||||
<div class="col-md-12 hidden-print" style="padding-top: 5px;">
|
<div class="col-md-12 hidden-print" style="padding-top: 5px;">
|
||||||
<a href='{{ route('modal.show', 'add-note') }}?type=asset&id={{$asset->id}}' style="width: 100%" data-toggle="modal" data-target="#createModal" data-select='add-note_select_id' data-refresh="assetHistory" data-hasnopayload="true" class="btn btn-sm btn-primary btn-block btn-social hidden-print">
|
<a href="#" style="width: 100%" data-toggle="modal" data-target="#createNoteModal" class="btn btn-sm btn-primary btn-block btn-social hidden-print">
|
||||||
<x-icon type="note" />
|
<x-icon type="note" />
|
||||||
{{ trans('general.add_note') }}</a>
|
{{ trans('general.add_note') }}
|
||||||
|
</a>
|
||||||
|
@include ('modals.add-note', ['type' => 'asset', 'id' => $asset->id])
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,35 @@
|
||||||
{{-- See snipeit_modals.js for what powers this --}}
|
{{-- See snipeit_modals.js for what powers this --}}
|
||||||
<div class="modal-dialog">
|
<div class="modal fade" id="createNoteModal" tabindex="-1" role="dialog" aria-labelledby="createNoteModalLabel" aria-hidden="true">
|
||||||
<div class="modal-content">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-content">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">{{ trans('general.add_note') }}</h2>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
</div>
|
<h2 class="modal-title" id="createNoteModalLabel">{{ trans('general.add_note') }}</h2>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="{{ route('notes.store') }}"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
>
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="type" value="{{$type}}"/>
|
||||||
|
<input type="hidden" name="id" value="{{$id}}"/>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="{{ route('api.notes.store') }}" onsubmit="return false">
|
<div class="alert alert-danger" id="modal_error_msg" style="display:none"></div>
|
||||||
<input type="hidden" name="type" value="{{request("type")}}"/>
|
|
||||||
<input type="hidden" name="id" value="{{request("id")}}"/>
|
|
||||||
<div class="alert alert-danger" id="modal_error_msg" style="display:none"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<textarea class="form-control" id="note" name="note">{{ old('note') }}</textarea>
|
<textarea class="form-control" id="note" name="note" required>{{ old('note') }}</textarea>
|
||||||
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">{{ trans('button.cancel') }}</button>
|
||||||
|
<button type="submit" class="btn btn-primary pull-right" id="modal-save">{{ trans('general.save') }}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div><!-- /.modal-content -->
|
||||||
<div class="modal-footer">
|
</div><!-- /.modal-dialog -->
|
||||||
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">{{ trans('button.cancel') }}</button>
|
</div>
|
||||||
<button type="button" class="btn btn-primary pull-right" id="modal-save">{{ trans('general.save') }}</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
|
|
|
@ -1302,20 +1302,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
|
||||||
)->name('api.activity.index');
|
)->name('api.activity.index');
|
||||||
}); // end reports api routes
|
}); // end reports api routes
|
||||||
|
|
||||||
/**
|
|
||||||
* Notes API routes
|
|
||||||
*/
|
|
||||||
|
|
||||||
Route::group(['prefix' => 'notes'], function () {
|
|
||||||
|
|
||||||
Route::post(
|
|
||||||
'/',
|
|
||||||
[ Api\NotesController::class,
|
|
||||||
'store'
|
|
||||||
]
|
|
||||||
)->name('api.notes.store');
|
|
||||||
}); // end notes api routes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,6 +16,7 @@ use App\Http\Controllers\LabelsController;
|
||||||
use App\Http\Controllers\LocationsController;
|
use App\Http\Controllers\LocationsController;
|
||||||
use App\Http\Controllers\ManufacturersController;
|
use App\Http\Controllers\ManufacturersController;
|
||||||
use App\Http\Controllers\ModalController;
|
use App\Http\Controllers\ModalController;
|
||||||
|
use App\Http\Controllers\NotesController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\ReportTemplatesController;
|
use App\Http\Controllers\ReportTemplatesController;
|
||||||
use App\Http\Controllers\ReportsController;
|
use App\Http\Controllers\ReportsController;
|
||||||
|
@ -468,6 +469,9 @@ Route::group(['prefix' => 'account', 'middleware' => ['auth']], function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group(['middleware' => ['auth']], function () {
|
||||||
|
Route::post('notes', [NotesController::class, 'store'])->name('notes.store');
|
||||||
|
});
|
||||||
|
|
||||||
Route::group(['prefix' => 'reports', 'middleware' => ['auth']], function () {
|
Route::group(['prefix' => 'reports', 'middleware' => ['auth']], function () {
|
||||||
|
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Notes;
|
|
||||||
|
|
||||||
use App\Events\NoteAdded;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AssetNotesTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testRequiresPermission()
|
|
||||||
{
|
|
||||||
$asset = Asset::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAsForApi(User::factory()->create())
|
|
||||||
->postJson(route('api.notes.store'), [
|
|
||||||
'note' => 'New Note!',
|
|
||||||
'type' => 'asset',
|
|
||||||
'id' => $asset->id,
|
|
||||||
])
|
|
||||||
->assertForbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidation()
|
|
||||||
{
|
|
||||||
$asset = Asset::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAsForApi(User::factory()->editAssets()->create())
|
|
||||||
->postJson(route('api.notes.store'), [
|
|
||||||
// 'note' => '',
|
|
||||||
'type' => 'a_type_not_asset',
|
|
||||||
'id' => $asset->id,
|
|
||||||
])
|
|
||||||
->assertOk()
|
|
||||||
->assertStatusMessageIs('error')
|
|
||||||
->assertJsonValidationErrors(['note', 'type'], 'messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRequiresExistingAsset()
|
|
||||||
{
|
|
||||||
$this->actingAsForApi(User::factory()->editAssets()->create())
|
|
||||||
->postJson(route('api.notes.store'), [
|
|
||||||
'note' => 'New Note!',
|
|
||||||
'type' => 'asset',
|
|
||||||
'id' => 999_999,
|
|
||||||
])
|
|
||||||
->assertStatusMessageIs('error')
|
|
||||||
->assertMessagesAre('Asset not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCanAddNoteToAsset()
|
|
||||||
{
|
|
||||||
Event::fake([NoteAdded::class]);
|
|
||||||
|
|
||||||
$asset = Asset::factory()->create();
|
|
||||||
$user = User::factory()->editAssets()->create();
|
|
||||||
|
|
||||||
$this->actingAsForApi($user)
|
|
||||||
->postJson(route('api.notes.store'), [
|
|
||||||
'note' => 'New Note!',
|
|
||||||
'type' => 'asset',
|
|
||||||
'id' => $asset->id,
|
|
||||||
])
|
|
||||||
->assertOk()
|
|
||||||
->assertStatusMessageIs('success');
|
|
||||||
|
|
||||||
Event::assertDispatchedTimes(NoteAdded::class, 1);
|
|
||||||
Event::assertDispatched(NoteAdded::class, function (NoteAdded $event) use ($asset, $user) {
|
|
||||||
return $event->itemNoteAddedOn->is($asset)
|
|
||||||
&& $event->note === 'New Note!'
|
|
||||||
&& $event->noteAddedBy->is($user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
70
tests/Feature/Notes/CreateNotesTest.php
Normal file
70
tests/Feature/Notes/CreateNotesTest.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Notes;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\User;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CreateNotesTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testRequiresPermission()
|
||||||
|
{
|
||||||
|
$this->actingAs(User::factory()->create())
|
||||||
|
->post(route('notes.store'))
|
||||||
|
->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidation()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->editAssets()->create())
|
||||||
|
->post(route('notes.store'), [
|
||||||
|
'id' => $asset->id,
|
||||||
|
// should be more...
|
||||||
|
])
|
||||||
|
->assertSessionHas('errors');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAssetMustExist()
|
||||||
|
{
|
||||||
|
$this->actingAs(User::factory()->editAssets()->create())
|
||||||
|
->post(route('notes.store'), [
|
||||||
|
'id' => 999_999,
|
||||||
|
'type' => 'asset',
|
||||||
|
'note' => 'my note',
|
||||||
|
])
|
||||||
|
->assertStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCanCreateNoteForAsset()
|
||||||
|
{
|
||||||
|
$actor = User::factory()->editAssets()->create();
|
||||||
|
|
||||||
|
$asset = Asset::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($actor)
|
||||||
|
->withHeader('User-Agent', 'Custom User Agent For Test')
|
||||||
|
->post(route('notes.store'), [
|
||||||
|
'_token' => '_token-to-simulate-request-from-gui',
|
||||||
|
'id' => $asset->id,
|
||||||
|
'type' => 'asset',
|
||||||
|
'note' => 'my special note',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('hardware.show', $asset->id) . '#history')
|
||||||
|
->assertSessionHas('success', trans('general.note_added'));
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('action_logs', [
|
||||||
|
'created_by' => $actor->id,
|
||||||
|
'action_type' => 'note added',
|
||||||
|
'target_id' => null,
|
||||||
|
'target_type' => null,
|
||||||
|
'note' => 'my special note',
|
||||||
|
'item_type' => Asset::class,
|
||||||
|
'item_id' => $asset->id,
|
||||||
|
'action_source' => 'gui',
|
||||||
|
'user_agent' => 'Custom User Agent For Test',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
namespace Tests\Unit\Listeners;
|
namespace Tests\Unit\Listeners;
|
||||||
|
|
||||||
use App\Events\CheckoutableCheckedOut;
|
use App\Events\CheckoutableCheckedOut;
|
||||||
use App\Events\NoteAdded;
|
|
||||||
use App\Listeners\LogListener;
|
use App\Listeners\LogListener;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
@ -38,19 +37,4 @@ class LogListenerTest extends TestCase
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLogsEntryOnAssetNoteCreation()
|
|
||||||
{
|
|
||||||
$asset = Asset::factory()->create();
|
|
||||||
$noteAddedBy = User::factory()->create();
|
|
||||||
|
|
||||||
event(new NoteAdded($asset, $noteAddedBy, 'My Cool Note!'));
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('action_logs', [
|
|
||||||
'action_type' => 'note_added',
|
|
||||||
'created_by' => $noteAddedBy->id,
|
|
||||||
'item_id' => $asset->id,
|
|
||||||
'item_type' => Asset::class,
|
|
||||||
'note' => 'My Cool Note!',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue