FEATURE #1373: add history of content
This commit contains only core funcionality of displaying history. Module settings must be implemented in each module.
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Handlers\Contracts;
|
||||||
|
|
||||||
|
use App\Models\Content;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
interface AppHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Render given module into visitor area.
|
||||||
|
*
|
||||||
|
* @param Content $content DB model from table contents
|
||||||
|
* @param array $parameters parameters used for modifications/custom settings of given model
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function render(Content $content, $parameters);
|
||||||
|
|
||||||
|
public static function getHistoryHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register admin menu (or multiple ones)
|
||||||
|
*
|
||||||
|
* This cannot be in ServiceProvider because Session is started at middleware. And ServiceProvider is called before
|
||||||
|
* starting any middleware..
|
||||||
|
*
|
||||||
|
* @param User $user model of logged user
|
||||||
|
*/
|
||||||
|
public static function registerAdminMenu(User $user);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Handlers\Contracts;
|
||||||
|
|
||||||
|
interface HistoryHandler
|
||||||
|
{
|
||||||
|
public static function parseColumnName($name);
|
||||||
|
|
||||||
|
public static function parseChangedValue($changeset);
|
||||||
|
}
|
||||||
@@ -1,28 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace App\Handlers;
|
namespace App\Handlers;
|
||||||
|
|
||||||
use App\Models\Content;
|
use App\Handlers\Contracts\AppHandler;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Handler
|
* Class Handler
|
||||||
*
|
*
|
||||||
* Handler is a class which handles module-specific behaviour which cannot be done by module itself, resp. cannot be done by some abstract magic.
|
* Handler is a class which handles module-specific behaviour which cannot be done by module itself, resp. cannot be
|
||||||
|
* done by some abstract magic.
|
||||||
*
|
*
|
||||||
* @package App\Handlers
|
* @package App\Handlers
|
||||||
*/
|
*/
|
||||||
abstract class Handler
|
abstract class Handler implements AppHandler
|
||||||
{
|
{
|
||||||
/**
|
public static function getHistoryHandler()
|
||||||
* Render given module into visitor area.
|
{
|
||||||
*
|
return History::class;
|
||||||
* @param Content $content DB model from table contents
|
}
|
||||||
* @param array $parameters parameters used for modifications/custom settings of given model
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract public function render(Content $content, $parameters);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register admin menu (or multiple ones)
|
* Register admin menu (or multiple ones)
|
||||||
@@ -34,6 +29,6 @@ abstract class Handler
|
|||||||
*/
|
*/
|
||||||
public static function registerAdminMenu(User $user)
|
public static function registerAdminMenu(User $user)
|
||||||
{
|
{
|
||||||
//
|
// core menu is registered in AdminMenuServiceProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Handlers;
|
||||||
|
|
||||||
|
use App\Enumerations\ContentStatuses;
|
||||||
|
use App\Handlers\Contracts\HistoryHandler;
|
||||||
|
use App\Helpers\Differ;
|
||||||
|
|
||||||
|
class History implements HistoryHandler
|
||||||
|
{
|
||||||
|
public static function parseColumnName($column)
|
||||||
|
{
|
||||||
|
return trans('admin/history.column' . ucfirst($column));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parseChangedValue($changeset)
|
||||||
|
{
|
||||||
|
switch ($changeset->column) {
|
||||||
|
case 'pageContent':
|
||||||
|
$route = route('history.diff', [
|
||||||
|
'revision' => $changeset->edit_batch,
|
||||||
|
'id' => $changeset->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return "(<a href='" . $route . "'>" . trans('admin/history.diff') . "</a>)";
|
||||||
|
break;
|
||||||
|
case 'metaDescription':
|
||||||
|
return Differ::htmlDiff($changeset->old_value, $changeset->new_value);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
$statuses = ContentStatuses::getWithTranslations();
|
||||||
|
|
||||||
|
return trans('admin/history.changedFromTo', [
|
||||||
|
'from' => $statuses[$changeset->old_value],
|
||||||
|
'to' => $statuses[$changeset->new_value]
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return trans('admin/history.changedFromTo', [
|
||||||
|
'from' => $changeset->old_value,
|
||||||
|
'to' => $changeset->new_value
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Paul's Simple Differ Algorithm v 0.1
|
||||||
|
(C) Paul Butler 2007 <http://www.paulbutler.org/>
|
||||||
|
May be used and distributed under the zlib/libpng license.
|
||||||
|
|
||||||
|
This code is intended for learning purposes; it was written with short
|
||||||
|
code taking priority over performance. It could be used in a practical
|
||||||
|
application, but there are a few ways it could be optimized.
|
||||||
|
|
||||||
|
Given two arrays, the function diff will return an array of the changes.
|
||||||
|
I won't describe the format of the array, but it will be obvious
|
||||||
|
if you use print_r() on the result of a diff on some test data.
|
||||||
|
|
||||||
|
htmlDiff is a wrapper for the diff command, it takes two strings and
|
||||||
|
returns the differences in HTML. The tags used are <ins> and <del>,
|
||||||
|
which can easily be styled with CSS.
|
||||||
|
|
||||||
|
@url https://github.com/paulgb/simplediff/blob/master/php/simplediff.php
|
||||||
|
|
||||||
|
Sorry for this kind of magic, I have no idea how this works.. :(
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Differ
|
||||||
|
{
|
||||||
|
public static function diff($old, $new)
|
||||||
|
{
|
||||||
|
$matrix = [];
|
||||||
|
$maxlen = 0;
|
||||||
|
foreach ($old as $oindex => $ovalue) {
|
||||||
|
$nkeys = array_keys($new, $ovalue);
|
||||||
|
foreach ($nkeys as $nindex) {
|
||||||
|
$matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ? $matrix[$oindex - 1][$nindex - 1] + 1 : 1;
|
||||||
|
if ($matrix[$oindex][$nindex] > $maxlen) {
|
||||||
|
$maxlen = $matrix[$oindex][$nindex];
|
||||||
|
$omax = $oindex + 1 - $maxlen;
|
||||||
|
$nmax = $nindex + 1 - $maxlen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($maxlen == 0) {
|
||||||
|
return [['d' => $old, 'i' => $new]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge(self::diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)), array_slice($new, $nmax, $maxlen), self::diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function htmlDiff($old, $new)
|
||||||
|
{
|
||||||
|
$ret = '';
|
||||||
|
$diff = self::diff(preg_split("/[\s]+/", $old), preg_split("/[\s]+/", $new));
|
||||||
|
foreach ($diff as $k) {
|
||||||
|
if (is_array($k)) {
|
||||||
|
$ret .= (!empty($k['d']) ? "<del>" . implode(' ', $k['d']) . "</del> " : '') . (!empty($k['i']) ? "<ins>" . implode(' ', $k['i']) . "</ins> " : '');
|
||||||
|
} else {
|
||||||
|
$ret .= $k . ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Models\Content;
|
||||||
|
use App\Models\ContentHistory;
|
||||||
|
|
||||||
|
class History extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ContentHistory
|
||||||
|
*/
|
||||||
|
private $history;
|
||||||
|
|
||||||
|
public function __construct(ContentHistory $history)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->history = $history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $id
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function getOverview($id)
|
||||||
|
{
|
||||||
|
// test if given content id is static page and if exists
|
||||||
|
$content = Content::findOrFail($id);
|
||||||
|
|
||||||
|
$history = $this->history->getStructuredHistoryOf($content);
|
||||||
|
|
||||||
|
return view('admin/history.overview', [
|
||||||
|
'history' => $history,
|
||||||
|
'page' => $content
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDiff($revision, $id)
|
||||||
|
{
|
||||||
|
$change = $this->history->findOrFail($id);
|
||||||
|
$diff = $this->history->getDiff($change);
|
||||||
|
|
||||||
|
return view('admin/history.diff', [
|
||||||
|
'change' => $change,
|
||||||
|
'columnName' => 'Page content',
|
||||||
|
'diff' => $diff
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,14 @@ Route::group([
|
|||||||
Route::get('users/overview/{state?}/{perPage?}', 'UsersOverview@getOverview')
|
Route::get('users/overview/{state?}/{perPage?}', 'UsersOverview@getOverview')
|
||||||
->name('users.overview');
|
->name('users.overview');
|
||||||
|
|
||||||
|
// history of changes for specified page
|
||||||
|
Route::get('history/{id}', 'History@getOverview')
|
||||||
|
->name('content.history');
|
||||||
|
|
||||||
|
// history of changes for specified page
|
||||||
|
Route::get('history/{revision}/{id}', 'History@getDiff')
|
||||||
|
->name('history.diff');
|
||||||
|
|
||||||
// custom fields
|
// custom fields
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Handlers\Contracts\AppHandler;
|
||||||
|
use App\Handlers\Contracts\HistoryHandler;
|
||||||
|
use App\Handlers\Handler;
|
||||||
|
use App\Helpers\Differ;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,10 +14,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
class ContentHistory extends Model
|
class ContentHistory extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'content_history';
|
protected $table = 'content_history';
|
||||||
|
|
||||||
// this table does not have created_at and updated_at fields
|
// this table does not have created_at and updated_at fields
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
'content_id',
|
'content_id',
|
||||||
@@ -22,8 +24,81 @@ class ContentHistory extends Model
|
|||||||
'new_value',
|
'new_value',
|
||||||
'old_value'
|
'old_value'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns history grouped by revisions (edit_batches)
|
||||||
|
*
|
||||||
|
* @param Content $content
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getStructuredHistoryOf(Content $content)
|
||||||
|
{
|
||||||
|
$data = $content->history()
|
||||||
|
->select('content_history.*', 'contents.module_handler', 'users.email AS user_email', 'users.name AS user_name')
|
||||||
|
->join('users', 'users.id', '=', 'content_history.user_id')
|
||||||
|
->join('contents', 'contents.id', '=', 'content_history.content_id')
|
||||||
|
->orderBy('changed_at', 'DESC')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$info = [];
|
||||||
|
$history = [];
|
||||||
|
foreach ($data as $change) {
|
||||||
|
$info[$change->edit_batch] = [
|
||||||
|
'changed_at' => $change->changed_at,
|
||||||
|
'user' => [
|
||||||
|
'id' => $change->user_id,
|
||||||
|
'name' => $change->user_name,
|
||||||
|
'email' => $change->user_email
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$change = $this->handleChangeset($change);
|
||||||
|
|
||||||
|
$history[$change->edit_batch][] = $change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'changesets' => $history,
|
||||||
|
'info' => $info
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $change
|
||||||
|
*
|
||||||
|
* @return HistoryHandler
|
||||||
|
*/
|
||||||
|
protected function getHistoryHandler($change)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var $handler AppHandler
|
||||||
|
* @var $historyHandler HistoryHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
// get handler of given content page
|
||||||
|
if ($change->column == 'module_settings') {
|
||||||
|
$handler = $handler = $change->module_handler;
|
||||||
|
} else {
|
||||||
|
$handler = Handler::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler::getHistoryHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleChangeset($change)
|
||||||
|
{
|
||||||
|
$historyHandler = $this->getHistoryHandler($change);
|
||||||
|
|
||||||
|
$change->column_trans = $historyHandler::parseColumnName($change->column);
|
||||||
|
$change->diff = $historyHandler::parseChangedValue($change);
|
||||||
|
|
||||||
|
return $change;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDiff($change)
|
||||||
|
{
|
||||||
|
return Differ::htmlDiff($change->old_value, $change->new_value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Vendored
+9
@@ -1,4 +1,13 @@
|
|||||||
|
|
||||||
|
del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
ins {
|
||||||
|
text-decoration: none;
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
.columns:last-child:not(:first-child), .column:last-child:not(:first-child) {
|
.columns:last-child:not(:first-child), .column:last-child:not(:first-child) {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'pageName' => 'History of :name',
|
||||||
|
'columnPageContent' => 'Page content',
|
||||||
|
'columnUrl' => 'URL',
|
||||||
|
'columnTitle' => 'Title',
|
||||||
|
'columnMetaKeywords' => 'Meta keywords',
|
||||||
|
'columnMetaDescription' => 'Description',
|
||||||
|
'columnStatus' => 'Page visibility',
|
||||||
|
'changedFromTo' => 'Changed from <del>:from</del> to <ins>:to</ins>',
|
||||||
|
'numOfChanges' => 'One change|:count changes',
|
||||||
|
'diff' => 'Difference',
|
||||||
|
'diffPageName' => 'Difference of :column',
|
||||||
|
'backToOverview' => 'Back to history overview'
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('pageName', @trans('admin/history.diffPageName', ['column' => $columnName]))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h2>@lang('admin/history.diffPageName', ['column' => $columnName])</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row callout">
|
||||||
|
{!! $diff !!}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<a href="{{route('content.history', ['id' => $change->content_id])}}" class="button success">
|
||||||
|
@lang('admin/history.backToOverview')
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@stop
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('pageName', @trans('admin/history.pageName', ['name' => $page->title]))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h2>@lang('admin/history.pageName', ['name' => $page->title])</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<ul class="accordion" data-accordion>
|
||||||
|
@foreach($history['changesets'] as $revision => $changes)
|
||||||
|
<li class="accordion-item" data-accordion-item>
|
||||||
|
<a class="accordion-title">
|
||||||
|
<strong>{{$history['info'][$revision]['user']['name']}}</strong> |
|
||||||
|
{{$history['info'][$revision]['changed_at']}} |
|
||||||
|
{{trans_choice('admin/history.numOfChanges', count($changes))}}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@foreach($changes as $change)
|
||||||
|
<div class="accordion-content" data-tab-content>
|
||||||
|
<strong>{{$change->column_trans}}</strong>: {!! $change->diff !!}
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@stop
|
||||||
Reference in New Issue
Block a user