#4: Nested albums are now supported in the admin panel

This commit is contained in:
Andy Heathershaw 2017-04-17 17:11:59 +01:00
parent e93e4d2413
commit 7ea1dc5c83
14 changed files with 253 additions and 41 deletions

View File

@ -19,7 +19,7 @@ class Album extends Model
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'description', 'url_alias', 'is_private', 'user_id', 'storage_id', 'default_view' 'name', 'description', 'url_alias', 'is_private', 'user_id', 'storage_id', 'default_view', 'parent_album_id'
]; ];
/** /**
@ -35,6 +35,11 @@ class Album extends Model
return $this->belongsToMany(Permission::class, 'album_anonymous_permissions'); return $this->belongsToMany(Permission::class, 'album_anonymous_permissions');
} }
public function children()
{
return $this->hasMany(Album::class, 'parent_album_id');
}
public function doesGroupHavePermission(Group $group, Permission $permission) public function doesGroupHavePermission(Group $group, Permission $permission)
{ {
return $this->groupPermissions()->where([ return $this->groupPermissions()->where([
@ -84,6 +89,31 @@ class Album extends Model
return $this->belongsToMany(Permission::class, 'album_group_permissions'); return $this->belongsToMany(Permission::class, 'album_group_permissions');
} }
/**
* Returns true if this album is a descendant of the given album.
* @param Album $album
*/
public function isChildOf(Album $album)
{
$currentAlbum = $this;
while (!is_null($currentAlbum))
{
if ($currentAlbum->parent_album_id == $album->id)
{
return true;
}
$currentAlbum = Album::where('id', $currentAlbum->parent_album_id)->first();
}
return false;
}
public function parent()
{
return $this->belongsTo(Album::class, 'parent_album_id');
}
public function photos() public function photos()
{ {
return $this->hasMany(Photo::class); return $this->hasMany(Photo::class);

View File

@ -9,10 +9,16 @@ use Illuminate\Support\Facades\Auth;
class DbHelper class DbHelper
{ {
public static function getAlbumsForCurrentUser() public static function getAlbumsForCurrentUser($parentID = -1)
{ {
return self::getAlbumsForCurrentUser_NonPaged() $query = self::getAlbumsForCurrentUser_NonPaged();
->paginate(UserConfig::get('items_per_page'));
if ($parentID == 0)
{
$query = $query->where('albums.parent_album_id', null);
}
return $query->paginate(UserConfig::get('items_per_page'));
} }
public static function getAlbumsForCurrentUser_NonPaged() public static function getAlbumsForCurrentUser_NonPaged()

View File

@ -14,6 +14,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests; use App\Http\Requests;
use App\Permission; use App\Permission;
use App\Photo; use App\Photo;
use App\Services\AlbumService;
use App\Services\PhotoService; use App\Services\PhotoService;
use App\Storage; use App\Storage;
use App\Upload; use App\Upload;
@ -71,11 +72,13 @@ class AlbumController extends Controller
return redirect(route('storage.create')); return redirect(route('storage.create'));
} }
$albumService = new AlbumService();
$defaultSource = Storage::where('is_default', true)->limit(1)->first(); $defaultSource = Storage::where('is_default', true)->limit(1)->first();
return Theme::render('admin.create_album', [ return Theme::render('admin.create_album', [
'album_sources' => $albumSources, 'album_sources' => $albumSources,
'default_storage_id' => (!is_null($defaultSource) ? $defaultSource->id : 0) 'default_storage_id' => (!is_null($defaultSource) ? $defaultSource->id : 0),
'parent_albums' => $albumService->getFlattenedAlbumTree()
]); ]);
} }
@ -133,7 +136,12 @@ class AlbumController extends Controller
$request->session()->flash('_old_input', $album->toArray()); $request->session()->flash('_old_input', $album->toArray());
} }
return Theme::render('admin.edit_album', ['album' => $album]); $albumService = new AlbumService();
return Theme::render('admin.edit_album', [
'album' => $album,
'parent_albums' => $albumService->getFlattenedAlbumTree()
]);
} }
/** /**
@ -145,7 +153,8 @@ class AlbumController extends Controller
{ {
$this->authorizeAccessToAdminPanel('admin:manage-albums'); $this->authorizeAccessToAdminPanel('admin:manage-albums');
$albums = DbHelper::getAlbumsForCurrentUser(); // Only get top-level albums
$albums = DbHelper::getAlbumsForCurrentUser(0);
return Theme::render('admin.list_albums', [ return Theme::render('admin.list_albums', [
'albums' => $albums, 'albums' => $albums,
@ -384,7 +393,12 @@ class AlbumController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-albums'); $this->authorizeAccessToAdminPanel('admin:manage-albums');
$album = new Album(); $album = new Album();
$album->fill($request->only(['name', 'description', 'storage_id'])); $album->fill($request->only(['name', 'description', 'storage_id', 'parent_album_id']));
if (strlen($album->parent_album_id) == 0)
{
$album->parent_album_id = null;
}
$album->default_view = UserConfig::get('default_album_view'); $album->default_view = UserConfig::get('default_album_view');
$album->is_private = (strtolower($request->get('is_private')) == 'on'); $album->is_private = (strtolower($request->get('is_private')) == 'on');
@ -408,9 +422,14 @@ class AlbumController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-albums'); $this->authorizeAccessToAdminPanel('admin:manage-albums');
$album = $this->loadAlbum($id); $album = $this->loadAlbum($id);
$album->fill($request->only(['name', 'description'])); $album->fill($request->only(['name', 'description', 'parent_album_id']));
$album->is_private = (strtolower($request->get('is_private')) == 'on'); $album->is_private = (strtolower($request->get('is_private')) == 'on');
if (strlen($album->parent_album_id) == 0)
{
$album->parent_album_id = null;
}
// These keys are optional and may or may not be in the request, depending on the page requesting it // These keys are optional and may or may not be in the request, depending on the page requesting it
foreach (['storage_id', 'default_view'] as $key) foreach (['storage_id', 'default_view'] as $key)
{ {

View File

@ -0,0 +1,42 @@
<?php
namespace App\Services;
use App\Album;
use App\Helpers\DbHelper;
class AlbumService
{
/**
* Get a list of all albums in a flattened tree-structure.
* @return mixed
*/
public function getFlattenedAlbumTree()
{
$allAlbums = DbHelper::getAlbumsForCurrentUser_NonPaged()->get();
$result = [];
$this->buildAlbumTree($result, $allAlbums);
return $result;
}
private function buildAlbumTree(array &$result, $allAlbums, Album $parent = null, $level = 0)
{
$parentAlbums = [];
foreach ($allAlbums as $album)
{
if ((is_null($parent) && is_null($album->parent_album_id)) || (!is_null($parent) && $album->parent_album_id == $parent->id))
{
$parentAlbums[] = $album;
}
}
foreach ($parentAlbums as $album)
{
$album->display_name = trim(sprintf('%s %s', str_repeat('-', $level), $album->name));
$result[$album->id] = $album;
$this->buildAlbumTree($result, $allAlbums, $album, $level + 1);
}
}
}

View File

@ -27,13 +27,13 @@ class CreateVisitorHitsTable extends Migration
$table->foreign('album_id') $table->foreign('album_id')
->references('id')->on('albums') ->references('id')->on('albums')
->onDelete('no action'); ->onDelete('cascade');
$table->foreign('photo_id') $table->foreign('photo_id')
->references('id')->on('photos') ->references('id')->on('photos')
->onDelete('no action'); ->onDelete('cascade');
$table->foreign('user_id') $table->foreign('user_id')
->references('id')->on('users') ->references('id')->on('users')
->onDelete('no action'); ->onDelete('cascade');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -14,12 +14,12 @@ class AttachHitColumns extends Migration
public function up() public function up()
{ {
Schema::table('albums', function (Blueprint $table) { Schema::table('albums', function (Blueprint $table) {
$table->bigInteger('hits'); $table->bigInteger('hits')->default(0);
}); });
Schema::table('photos', function (Blueprint $table) { Schema::table('photos', function (Blueprint $table) {
$table->bigInteger('hits'); $table->bigInteger('hits')->default(0);
$table->bigInteger('hits_download'); $table->bigInteger('hits_download')->default(0);
}); });
} }

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddParentAlbumColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('albums', function (Blueprint $table)
{
$table->unsignedInteger('parent_album_id')->nullable();
$table->foreign('parent_album_id')
->references('id')->on('albums')
->onDelete('set null');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('albums', function (Blueprint $table)
{
$table->dropForeign('albums_parent_album_id_foreign');
$table->dropColumn('parent_album_id');
});
}
}

View File

@ -2,6 +2,11 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
.album-expand-handle {
cursor: pointer;
margin-top: 5px;
}
.card-header.card-danger { .card-header.card-danger {
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;

View File

@ -19,6 +19,10 @@ textarea {
margin-top: 20px; margin-top: 20px;
} }
.hidden {
display: none;
}
.tab-content { .tab-content {
border: solid 1px rgb(221, 221, 221); border: solid 1px rgb(221, 221, 221);
border-top: 0; border-top: 0;

View File

@ -19,6 +19,8 @@ return [
'email_label' => 'E-mail address:', 'email_label' => 'E-mail address:',
'login_action' => 'Login', 'login_action' => 'Login',
'name_label' => 'Name:', 'name_label' => 'Name:',
'parent_album_label' => 'Parent album:',
'parent_album_placeholder' => 'None (top-level album)',
'password_label' => 'Password:', 'password_label' => 'Password:',
'password_confirm_label' => 'Confirm password:', 'password_confirm_label' => 'Confirm password:',
'private_album_label' => 'Private album (only visible to me)', 'private_album_label' => 'Private album (only visible to me)',
@ -27,6 +29,7 @@ return [
'remember_me_label' => 'Remember me', 'remember_me_label' => 'Remember me',
'remove_action' => 'Remove', 'remove_action' => 'Remove',
'select' => 'Select', 'select' => 'Select',
'select_current_text' => '(current)',
'settings_hotlink_protection' => 'Prevent hot-linking to images', 'settings_hotlink_protection' => 'Prevent hot-linking to images',
'settings_hotlink_protection_help' => 'With this option enabled, direct linking to images is not allowed. Photos can only be viewed through Blue Twilight.', 'settings_hotlink_protection_help' => 'With this option enabled, direct linking to images is not allowed. Photos can only be viewed through Blue Twilight.',
'settings_restrict_originals_download' => 'Restrict access to original images', 'settings_restrict_originals_download' => 'Restrict access to original images',

View File

@ -35,9 +35,19 @@
<textarea class="form-control" id="album-description" name="description" rows="5">{{ old('description') }}</textarea> <textarea class="form-control" id="album-description" name="description" rows="5">{{ old('description') }}</textarea>
</div> </div>
<div class="form-group">
<label class="form-control-label" for="parent-album">@lang('forms.parent_album_label')</label>
<select class="form-control" name="parent_album_id" id="parent-album">
<option value="">@lang('forms.parent_album_placeholder')</option>
@foreach ($parent_albums as $key => $value)
<option value="{{ $key }}"{{ $key == old('parent_album_id') ? ' selected="selected"' : '' }}>{{ $value->display_name }}</option>
@endforeach
</select>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-control-label" for="album-source">@lang('forms.album_source_label')</label> <label class="form-control-label" for="album-source">@lang('forms.album_source_label')</label>
<select class="form-control" name="storage_id"> <select class="form-control" name="storage_id" id="album-source">
@foreach ($album_sources as $key => $value) @foreach ($album_sources as $key => $value)
<option value="{{ $key }}"{{ $key == old('storage_id') ? ' selected="selected"' : '' }}>{{ $value }}</option> <option value="{{ $key }}"{{ $key == old('storage_id') ? ' selected="selected"' : '' }}>{{ $value }}</option>
@endforeach @endforeach

View File

@ -44,6 +44,16 @@
@endif @endif
</div> </div>
<div class="form-group">
<label class="form-control-label" for="parent-album">@lang('forms.parent_album_label')</label>
<select class="form-control" name="parent_album_id" id="parent-album">
<option value="">@lang('forms.parent_album_placeholder')</option>
@foreach ($parent_albums as $key => $value)
<option value="{{ $key }}"{{ $key == $album->id || $value->isChildOf($album) ? ' disabled="disabled"' : '' }}{{ $key == old('parent_album_id') ? ' selected="selected"' : '' }}>{{ $value->display_name }}{{ $key == old('parent_album_id') ? ' ' . trans('forms.select_current_text') : '' }}</option>
@endforeach
</select>
</div>
<div class="text-right"> <div class="text-right">
<a href="{{ route('albums.show', ['id' => $album->id]) }}" class="btn btn-link">@lang('forms.cancel_action')</a> <a href="{{ route('albums.show', ['id' => $album->id]) }}" class="btn btn-link">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button> <button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button>

View File

@ -28,30 +28,7 @@
<table class="table table-hover table-striped"> <table class="table table-hover table-striped">
<tbody> <tbody>
@foreach ($albums as $album) @foreach ($albums as $album)
<tr> @include (Theme::viewName('partials.single_album_admin'), ['is_child' => false])
<td>
<span style="font-size: 1.3em;">
@can('edit', $album)
<a href="{{ route('albums.show', ['id' => $album->id]) }}">{{ $album->name }}</a>
@endcan
@cannot('edit', $album)
{{ $album->name }} <i class="fa fa-fw fa-lock"></i>
@endcannot
</span><br/>
<p>{{ $album->description }}</p>
<p style="margin-bottom: 0;"><b>{{ $album->photos_count }}</b> {{ trans_choice('admin.stats_widget.photos', $album->photos_count) }}</p>
</td>
<td class="text-right">
<div class="btn-group">
@can('edit', $album)
<a href="{{ route('albums.edit', ['id' => $album->id]) }}" class="btn btn-secondary">@lang('forms.edit_action')</a>
@endcan
@can('delete', $album)
<a href="{{ route('albums.delete', ['id' => $album->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
@endcan
</div>
</td>
</tr>
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
@ -68,3 +45,38 @@
</div> </div>
</div> </div>
@endsection @endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('.album-expand-handle').click(function() {
var parent = $(this).closest('tr');
var handle = $('.album-expand-handle', parent);
var albumID = parent.data('album-id');
$('tr[data-parent-album-id=' + albumID + ']').toggle();
if (handle.hasClass('fa-plus'))
{
handle.addClass('fa-minus');
handle.removeClass('fa-plus');
}
else
{
// Toggle all children
$('tr[data-parent-album-id=' + albumID + ']').each(function(index, element)
{
var childHandle = $('.album-expand-handle', element);
if (childHandle.hasClass('fa-minus'))
{
childHandle.click();
}
});
handle.addClass('fa-plus');
handle.removeClass('fa-minus');
}
})
})
</script>
@endpush

View File

@ -0,0 +1,32 @@
<tr data-album-id="{{ $album->id }}" class="{{ $is_child ? 'hidden' : '' }}" @if (!is_null($album->parent_album_id)) data-parent-album-id="{{ $album->parent_album_id }}" @endif>
<td style="width: 20px;">
@if ($album->children()->count() > 0)
<i class="album-expand-handle fa fa-fw fa-plus mt-2"></i>
@endif
</td>
<td>
<span style="font-size: 1.3em;">
@can('edit', $album)
<a href="{{ route('albums.show', ['id' => $album->id]) }}">{{ $album->name }}</a>
@endcan
@cannot('edit', $album)
{{ $album->name }} <i class="fa fa-fw fa-lock"></i>
@endcannot
</span><br/>
<p>{{ $album->description }}</p>
<p style="margin-bottom: 0;"><b>{{ $album->photos_count }}</b> {{ trans_choice('admin.stats_widget.photos', $album->photos_count) }}</p>
</td>
<td class="text-right">
<div class="btn-group">
@can('edit', $album)
<a href="{{ route('albums.edit', ['id' => $album->id]) }}" class="btn btn-secondary">@lang('forms.edit_action')</a>
@endcan
@can('delete', $album)
<a href="{{ route('albums.delete', ['id' => $album->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
@endcan
</div>
</td>
</tr>
@foreach ($album->children as $album)
@include (Theme::viewName('partials.single_album_admin'), ['is_child' => true])
@endforeach