1
0
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:
Kovah 2022-06-09 22:20:32 +02:00
parent ac13db9d5c
commit 18089253fe
No known key found for this signature in database
GPG Key ID: AAAA031BA9830D7B
15 changed files with 398 additions and 12 deletions

View File

@ -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'),

View File

@ -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'),

View File

@ -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,

View File

@ -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
*/

View File

@ -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
*/

View 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];
}
}

View 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];
}
}

View File

@ -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',

View File

@ -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',

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View 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);
}
}

View 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);
}
}