mirror of
https://github.com/Kovah/LinkAce.git
synced 2025-01-17 13:18:21 +01:00
Add audit log to lists and tags (#467)
This commit is contained in:
parent
ac13db9d5c
commit
18089253fe
@ -96,6 +96,7 @@ class ListController extends Controller
|
||||
|
||||
return view('models.lists.show', [
|
||||
'list' => $list,
|
||||
'history' => $list->audits()->latest()->get(),
|
||||
'listLinks' => $links,
|
||||
'route' => $request->getBaseUrl(),
|
||||
'orderBy' => $request->input('orderBy', 'created_at'),
|
||||
|
@ -97,6 +97,7 @@ class TagController extends Controller
|
||||
|
||||
return view('models.tags.show', [
|
||||
'tag' => $tag,
|
||||
'history' => $tag->audits()->latest()->get(),
|
||||
'tagLinks' => $links,
|
||||
'route' => $request->getBaseUrl(),
|
||||
'orderBy' => $request->input('orderBy', 'created_at'),
|
||||
|
@ -89,7 +89,7 @@ class Link extends Model implements Auditable
|
||||
'icon',
|
||||
];
|
||||
|
||||
public $auditModifiers = [
|
||||
public array $auditModifiers = [
|
||||
'is_private' => BooleanModifier::class,
|
||||
'check_disabled' => BooleanModifier::class,
|
||||
'status' => LinkStatusModifier::class,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Audits\Modifiers\BooleanModifier;
|
||||
use App\Scopes\OrderNameScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@ -12,6 +13,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use OwenIt\Auditing\Auditable as AuditableTrait;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
|
||||
/**
|
||||
* Class LinkList
|
||||
@ -31,8 +34,9 @@ use Illuminate\Support\Str;
|
||||
* @method static Builder|Tag privateOnly()
|
||||
* @method static Builder|Tag publicOnly()
|
||||
*/
|
||||
class LinkList extends Model
|
||||
class LinkList extends Model implements Auditable
|
||||
{
|
||||
use AuditableTrait;
|
||||
use SoftDeletes;
|
||||
use HasFactory;
|
||||
|
||||
@ -50,6 +54,11 @@ class LinkList extends Model
|
||||
'is_private' => 'boolean',
|
||||
];
|
||||
|
||||
// Audit settings
|
||||
public array $auditModifiers = [
|
||||
'is_private' => BooleanModifier::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Add the OrderNameScope to the Tag model
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Audits\Modifiers\BooleanModifier;
|
||||
use App\Scopes\OrderNameScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@ -11,6 +12,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use OwenIt\Auditing\Auditable as AuditableTrait;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
|
||||
/**
|
||||
* Class Tag
|
||||
@ -29,8 +32,9 @@ use Illuminate\Support\Carbon;
|
||||
* @method static Builder|Tag publicOnly()
|
||||
* @method static Builder|Tag privateOnly()
|
||||
*/
|
||||
class Tag extends Model
|
||||
class Tag extends Model implements Auditable
|
||||
{
|
||||
use AuditableTrait;
|
||||
use SoftDeletes;
|
||||
use HasFactory;
|
||||
|
||||
@ -45,6 +49,11 @@ class Tag extends Model
|
||||
'is_private' => 'boolean',
|
||||
];
|
||||
|
||||
// Audit settings
|
||||
public array $auditModifiers = [
|
||||
'is_private' => BooleanModifier::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Add the OrderNameScope to the Tag model
|
||||
*/
|
||||
|
86
app/View/Components/History/ListEntry.php
Normal file
86
app/View/Components/History/ListEntry.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components\History;
|
||||
|
||||
use App\Models\Link;
|
||||
use Illuminate\View\Component;
|
||||
use OwenIt\Auditing\Models\Audit;
|
||||
|
||||
class ListEntry extends Component
|
||||
{
|
||||
public function __construct(private Audit $entry, private array $changes = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$timestamp = formatDateTime($this->entry->created_at);
|
||||
|
||||
if ($this->entry->event === 'deleted') {
|
||||
$this->changes[] = trans('list.history_deleted');
|
||||
} elseif ($this->entry->event === 'restored') {
|
||||
$this->changes[] = trans('list.history_restored');
|
||||
} else {
|
||||
foreach ($this->entry->getModified() as $field => $change) {
|
||||
$this->processChange($field, $change);
|
||||
}
|
||||
}
|
||||
|
||||
return view('components.history-entry', [
|
||||
'timestamp' => $timestamp,
|
||||
'changes' => $this->changes,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function processChange(string $field, array $changeData): void
|
||||
{
|
||||
$fieldName = trans('list.' . $field);
|
||||
[$oldValue, $newValue] = $this->processValues($field, $changeData);
|
||||
|
||||
if ($oldValue === null) {
|
||||
$change = trans('linkace.history_added', [
|
||||
'fieldname' => $fieldName,
|
||||
'newvalue' => htmlspecialchars($newValue),
|
||||
]);
|
||||
} elseif ($newValue === null) {
|
||||
$change = trans('linkace.history_removed', [
|
||||
'fieldname' => $fieldName,
|
||||
'oldvalue' => htmlspecialchars($oldValue),
|
||||
]);
|
||||
} else {
|
||||
$change = trans('linkace.history_changed', [
|
||||
'fieldname' => $fieldName,
|
||||
'oldvalue' => htmlspecialchars($oldValue),
|
||||
'newvalue' => htmlspecialchars($newValue),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->changes[] = $change;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply specialized methods for different fields to handle particular
|
||||
* formatting needs of these fields.
|
||||
*
|
||||
* @param string $field
|
||||
* @param array $changeData
|
||||
* @return array
|
||||
*/
|
||||
protected function processValues(string $field, array $changeData): array
|
||||
{
|
||||
$oldValue = $changeData['old'] ?? null;
|
||||
$newValue = $changeData['new'] ?? null;
|
||||
|
||||
/** @var Link $model */
|
||||
$model = app($this->entry->auditable_type);
|
||||
|
||||
if (isset($model->auditModifiers[$field])) {
|
||||
$modifier = app($model->auditModifiers[$field]);
|
||||
$oldValue = $modifier->modify($oldValue);
|
||||
$newValue = $modifier->modify($newValue);
|
||||
return [$oldValue, $newValue];
|
||||
}
|
||||
|
||||
return [$oldValue, $newValue];
|
||||
}
|
||||
}
|
86
app/View/Components/History/TagEntry.php
Normal file
86
app/View/Components/History/TagEntry.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components\History;
|
||||
|
||||
use App\Models\Link;
|
||||
use Illuminate\View\Component;
|
||||
use OwenIt\Auditing\Models\Audit;
|
||||
|
||||
class TagEntry extends Component
|
||||
{
|
||||
public function __construct(private Audit $entry, private array $changes = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$timestamp = formatDateTime($this->entry->created_at);
|
||||
|
||||
if ($this->entry->event === 'deleted') {
|
||||
$this->changes[] = trans('tag.history_deleted');
|
||||
} elseif ($this->entry->event === 'restored') {
|
||||
$this->changes[] = trans('tag.history_restored');
|
||||
} else {
|
||||
foreach ($this->entry->getModified() as $field => $change) {
|
||||
$this->processChange($field, $change);
|
||||
}
|
||||
}
|
||||
|
||||
return view('components.history-entry', [
|
||||
'timestamp' => $timestamp,
|
||||
'changes' => $this->changes,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function processChange(string $field, array $changeData): void
|
||||
{
|
||||
$fieldName = trans('tag.' . $field);
|
||||
[$oldValue, $newValue] = $this->processValues($field, $changeData);
|
||||
|
||||
if ($oldValue === null) {
|
||||
$change = trans('linkace.history_added', [
|
||||
'fieldname' => $fieldName,
|
||||
'newvalue' => htmlspecialchars($newValue),
|
||||
]);
|
||||
} elseif ($newValue === null) {
|
||||
$change = trans('linkace.history_removed', [
|
||||
'fieldname' => $fieldName,
|
||||
'oldvalue' => htmlspecialchars($oldValue),
|
||||
]);
|
||||
} else {
|
||||
$change = trans('linkace.history_changed', [
|
||||
'fieldname' => $fieldName,
|
||||
'oldvalue' => htmlspecialchars($oldValue),
|
||||
'newvalue' => htmlspecialchars($newValue),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->changes[] = $change;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply specialized methods for different fields to handle particular
|
||||
* formatting needs of these fields.
|
||||
*
|
||||
* @param string $field
|
||||
* @param array $changeData
|
||||
* @return array
|
||||
*/
|
||||
protected function processValues(string $field, array $changeData): array
|
||||
{
|
||||
$oldValue = $changeData['old'] ?? null;
|
||||
$newValue = $changeData['new'] ?? null;
|
||||
|
||||
/** @var Link $model */
|
||||
$model = app($this->entry->auditable_type);
|
||||
|
||||
if (isset($model->auditModifiers[$field])) {
|
||||
$modifier = app($model->auditModifiers[$field]);
|
||||
$oldValue = $modifier->modify($oldValue);
|
||||
$newValue = $modifier->modify($newValue);
|
||||
return [$oldValue, $newValue];
|
||||
}
|
||||
|
||||
return [$oldValue, $newValue];
|
||||
}
|
||||
}
|
@ -17,6 +17,11 @@ return [
|
||||
|
||||
'name' => 'List Name',
|
||||
'description' => 'List Description',
|
||||
'is_private' => 'Private Status',
|
||||
|
||||
'history_deleted' => 'List was deleted',
|
||||
'history_restored' => 'List was restored',
|
||||
'history_created' => 'List was created',
|
||||
|
||||
'author' => 'by :user',
|
||||
|
||||
|
@ -16,6 +16,11 @@ return [
|
||||
'private' => 'Private Tag',
|
||||
|
||||
'name' => 'Tag Name',
|
||||
'is_private' => 'Private Status',
|
||||
|
||||
'history_deleted' => 'Tag was deleted',
|
||||
'history_restored' => 'Tag was restored',
|
||||
'history_created' => 'Tag was created',
|
||||
|
||||
'author' => 'by :user',
|
||||
|
||||
|
@ -168,7 +168,7 @@
|
||||
<div class="link-history mt-5">
|
||||
<h3 class="h6 mb-2">@lang('link.history')</h3>
|
||||
|
||||
<div class="small text-muted">
|
||||
<div class="history small text-muted">
|
||||
@foreach($history as $entry)
|
||||
@if($loop->index === 5 && $loop->count >= 10)
|
||||
<a data-bs-toggle="collapse" href="#link-history" role="button" class="d-inline-block mb-1"
|
||||
|
@ -48,12 +48,32 @@
|
||||
@lang('link.links')
|
||||
</div>
|
||||
<div class="card-table">
|
||||
|
||||
@include('models.links.partials.table', ['links' => $listLinks])
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! $listLinks->onEachSide(1)->withQueryString()->links() !!}
|
||||
|
||||
<div class="list-history mt-5">
|
||||
<h3 class="h6 mb-2">@lang('linkace.history')</h3>
|
||||
|
||||
<div class="history small text-muted">
|
||||
@foreach($history as $entry)
|
||||
@if($loop->index === 5 && $loop->count >= 10)
|
||||
<a data-bs-toggle="collapse" href="#list-history" role="button" class="d-inline-block mb-1"
|
||||
aria-expanded="false" aria-controls="list-history">
|
||||
@lang('linkace.more')
|
||||
<x-icon.caret-down class="fw"/>
|
||||
</a>
|
||||
<div id="list-history" class="collapse">
|
||||
@endif
|
||||
<x-history.list-entry :entry="$entry"/>
|
||||
@endforeach
|
||||
<div>{{ formatDateTime($list->created_at) }}: @lang('list.history_created')</div>
|
||||
@if(count($history) >= 10)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
@ -47,4 +47,26 @@
|
||||
|
||||
{!! $tagLinks->onEachSide(1)->withQueryString()->links() !!}
|
||||
|
||||
<div class="list-history mt-5">
|
||||
<h3 class="h6 mb-2">@lang('linkace.history')</h3>
|
||||
|
||||
<div class="history small text-muted">
|
||||
@foreach($history as $entry)
|
||||
@if($loop->index === 5 && $loop->count >= 10)
|
||||
<a data-bs-toggle="collapse" href="#tag-history" role="button" class="d-inline-block mb-1"
|
||||
aria-expanded="false" aria-controls="tag-history">
|
||||
@lang('linkace.more')
|
||||
<x-icon.caret-down class="fw"/>
|
||||
</a>
|
||||
<div id="tag-history" class="collapse">
|
||||
@endif
|
||||
<x-history.tag-entry :entry="$entry"/>
|
||||
@endforeach
|
||||
<div>{{ formatDateTime($tag->created_at) }}: @lang('tag.history_created')</div>
|
||||
@if(count($history) >= 10)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Components;
|
||||
namespace Tests\Components\History;
|
||||
|
||||
use App\Models\Link;
|
||||
use App\Models\LinkList;
|
||||
@ -10,18 +10,16 @@ use App\View\Components\History\LinkEntry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HistoryEntryTest extends TestCase
|
||||
class LinkEntryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user);
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
}
|
||||
|
||||
public function testAddedChange(): void
|
86
tests/Components/History/ListEntryTest.php
Normal file
86
tests/Components/History/ListEntryTest.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Components\History;
|
||||
|
||||
use App\Models\LinkList;
|
||||
use App\Models\User;
|
||||
use App\View\Components\History\ListEntry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ListEntryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
}
|
||||
|
||||
public function testAddedChange(): void
|
||||
{
|
||||
$list = LinkList::factory()->create([
|
||||
'description' => null,
|
||||
]);
|
||||
|
||||
$list->update(['description' => 'Test Description']);
|
||||
|
||||
$historyEntry = $list->audits()->first();
|
||||
|
||||
$output = (new ListEntry($historyEntry))->render();
|
||||
|
||||
$this->assertStringContainsString('Added <code>Test Description</code> to List Description', $output);
|
||||
}
|
||||
|
||||
public function testRegularChange(): void
|
||||
{
|
||||
$list = LinkList::factory()->create([
|
||||
'description' => 'Test Description',
|
||||
]);
|
||||
|
||||
$list->update(['description' => 'New Description']);
|
||||
|
||||
$historyEntry = $list->audits()->first();
|
||||
|
||||
$output = (new ListEntry($historyEntry))->render();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Changed List Description from <code>Test Description</code> to <code>New Description</code>',
|
||||
$output
|
||||
);
|
||||
}
|
||||
|
||||
public function testRemoveChange(): void
|
||||
{
|
||||
$list = LinkList::factory()->create([
|
||||
'description' => 'Test Description',
|
||||
]);
|
||||
|
||||
$list->update(['description' => null]);
|
||||
|
||||
$historyEntry = $list->audits()->first();
|
||||
|
||||
$output = (new ListEntry($historyEntry))->render();
|
||||
|
||||
$this->assertStringContainsString('Removed <code>Test Description</code> from List Description', $output);
|
||||
}
|
||||
|
||||
public function testModelDeletion(): void
|
||||
{
|
||||
$list = LinkList::factory()->create();
|
||||
|
||||
$list->delete();
|
||||
$list->restore();
|
||||
|
||||
$historyEntries = $list->audits()->get();
|
||||
|
||||
$output = (new ListEntry($historyEntries[0]))->render();
|
||||
$this->assertStringContainsString('List was deleted', $output);
|
||||
|
||||
$output = (new ListEntry($historyEntries[1]))->render();
|
||||
$this->assertStringContainsString('List was restored', $output);
|
||||
}
|
||||
}
|
58
tests/Components/History/TagEntryTest.php
Normal file
58
tests/Components/History/TagEntryTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Components\History;
|
||||
|
||||
use App\Models\LinkList;
|
||||
use App\Models\Tag;
|
||||
use App\Models\User;
|
||||
use App\View\Components\History\ListEntry;
|
||||
use App\View\Components\History\TagEntry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TagEntryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
}
|
||||
|
||||
public function testRegularChange(): void
|
||||
{
|
||||
$tag = Tag::factory()->create([
|
||||
'name' => 'Test Tag',
|
||||
]);
|
||||
|
||||
$tag->update(['name' => 'New Tag']);
|
||||
|
||||
$historyEntry = $tag->audits()->first();
|
||||
|
||||
$output = (new TagEntry($historyEntry))->render();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Changed Tag Name from <code>Test Tag</code> to <code>New Tag</code>',
|
||||
$output
|
||||
);
|
||||
}
|
||||
|
||||
public function testModelDeletion(): void
|
||||
{
|
||||
$tag = Tag::factory()->create();
|
||||
|
||||
$tag->delete();
|
||||
$tag->restore();
|
||||
|
||||
$historyEntries = $tag->audits()->get();
|
||||
|
||||
$output = (new TagEntry($historyEntries[0]))->render();
|
||||
$this->assertStringContainsString('Tag was deleted', $output);
|
||||
|
||||
$output = (new TagEntry($historyEntries[1]))->render();
|
||||
$this->assertStringContainsString('Tag was restored', $output);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user