#19: First draft of the new user profile page, incorporating the beginnings of a heat-map of activity

This commit is contained in:
Andy Heathershaw 2018-07-16 06:04:44 +01:00
parent cd2dcc22a2
commit 33680faf92
15 changed files with 424 additions and 3 deletions

View File

@ -111,6 +111,7 @@ class ConfigHelper
'smtp_password' => '', 'smtp_password' => '',
'smtp_port' => 25, 'smtp_port' => 25,
'smtp_username' => '', 'smtp_username' => '',
'social_user_profiles' => false,
'theme' => 'default' 'theme' => 'default'
); );
} }

View File

@ -223,6 +223,7 @@ class DefaultController extends Controller
'require_email_verification', 'require_email_verification',
'restrict_original_download', 'restrict_original_download',
'smtp_encryption', 'smtp_encryption',
'social_user_profiles'
]; ];
$updateKeys = [ $updateKeys = [
'albums_menu_number_items', 'albums_menu_number_items',

View File

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Album;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Request;
class UserController extends Controller
{
public function show(Request $request, $idOrAlias)
{
// If a user has a profile alias set, their profile page cannot be accessed by the ID
$user = User::where(DB::raw('COALESCE(profile_alias, id)'), strtolower($idOrAlias))->first();
if (is_null($user))
{
App::abort(404);
return null;
}
$this->authorizeForUser($this->getUser(), 'view', $user);
$albums = $this->getAlbumsForUser($user);
$albumIDs = $this->getAlbumIDsForUser($user);
$cameras = $this->getCamerasUsedInAlbums($albumIDs);
$activity = $this->getActivityDatesInAlbums($albumIDs);
return Theme::render('gallery.user_profile', [
'activity_taken' => $this->constructActivityGrid($activity['taken']),
'activity_uploaded' => $this->constructActivityGrid($activity['uploaded']),
'albums' => $albums,
'cameras' => $cameras,
'user' => $user
]);
}
private function constructActivityGrid(Collection $collection)
{
$results = [];
$lastYearFrom = new \DateTime();
$lastYearFrom->sub(new \DateInterval('P1Y'));
$lastYearFrom->add(new \DateInterval('P1D'));
$today = new \DateTime();
$current = clone $lastYearFrom;
while ($current < $today)
{
$year = intval($current->format('Y'));
$month = intval($current->format('m'));
$date = intval($current->format('d'));
if (!isset($results[$year]))
{
$results[$year] = [];
}
if (!isset($results[$year][$month]))
{
$results[$year][$month] = [];
}
if (!isset($results[$year][$month][$date]))
{
$results[$year][$month][$date] = 0;
}
$current->add(new \DateInterval('P1D'));
}
// Now update the totals from the collection
foreach ($collection as $photoInfo)
{
$date = \DateTime::createFromFormat('Y-m-d', $photoInfo->the_date);
$year = intval($date->format('Y'));
$month = intval($date->format('m'));
$date = intval($date->format('d'));
$results[$year][$month][$date] = $photoInfo->photos_count;
}
// Replace the month names
foreach ($results as $year => &$months)
{
foreach ($months as $month => $dates)
{
$monthDate = \DateTime::createFromFormat('m', $month);
$months[$monthDate->format('M')] = $dates;
unset($months[$month]);
}
}
return $results;
}
private function getActivityDatesInAlbums(array $albumIDs)
{
$createdAt = DB::table('photos')
->whereIn('album_id', $albumIDs)
->whereRaw(DB::raw('DATE(created_at) > DATE(DATE_SUB(NOW(), INTERVAL 1 year))'))
->select([
DB::raw('DATE(created_at) AS the_date'),
DB::raw('COUNT(photos.id) AS photos_count')
])
->groupBy(DB::raw('DATE(created_at)'))
->orderBy(DB::raw('DATE(created_at)'))
->get();
$takenAt = DB::table('photos')
->whereIn('album_id', $albumIDs)
->whereRaw(DB::raw('DATE(taken_at) > DATE(DATE_SUB(NOW(), INTERVAL 1 year))'))
->select([
DB::raw('DATE(taken_at) AS the_date'),
DB::raw('COUNT(photos.id) AS photos_count')
])
->groupBy(DB::raw('DATE(taken_at)'))
->orderBy(DB::raw('DATE(taken_at)'))
->get();
return ['uploaded' => $createdAt, 'taken' => $takenAt];
}
private function getAlbumsForUser(User $user)
{
return DbHelper::getAlbumsForCurrentUser_NonPaged()
->where('user_id', $user->id)
->paginate(UserConfig::get('items_per_page'));
}
private function getAlbumIDsForUser(User $user)
{
$results = [];
$albums = DbHelper::getAlbumsForCurrentUser_NonPaged()
->where('user_id', $user->id)
->select('albums.id')
->get();
foreach ($albums as $album)
{
$results[] = intval($album->id);
}
return $results;
}
private function getCamerasUsedInAlbums(array $albumIDs)
{
return DB::table('photos')
->whereIn('album_id', $albumIDs)
->where([
['camera_make', '!=', ''],
['camera_model', '!=', '']
])
->groupBy('camera_make', 'camera_model', 'camera_software')
->select('camera_make', 'camera_model', 'camera_software', DB::raw('count(*) as photo_count'))
->orderBy('photo_count', 'desc')
->orderBy('camera_make')
->orderBy('camera_model')
->orderBy('camera_software')
->get();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Policies;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
public function before($user, $ability)
{
if (!UserConfig::get('social_user_profiles'))
{
// Social profiles not enabled
return false;
}
}
public function view(User $user, User $userBeingAccessed)
{
// TODO per-user setting that determines if the page is available
return true;
}
}

View File

@ -9,6 +9,7 @@ use App\Permission;
use App\Photo; use App\Photo;
use App\Policies\AlbumPolicy; use App\Policies\AlbumPolicy;
use App\Policies\PhotoPolicy; use App\Policies\PhotoPolicy;
use App\Policies\UserPolicy;
use App\User; use App\User;
use function GuzzleHttp\Psr7\mimetype_from_extension; use function GuzzleHttp\Psr7\mimetype_from_extension;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@ -28,7 +29,8 @@ class AuthServiceProvider extends ServiceProvider
*/ */
protected $policies = [ protected $policies = [
Album::class => AlbumPolicy::class, Album::class => AlbumPolicy::class,
Photo::class => PhotoPolicy::class Photo::class => PhotoPolicy::class,
User::class => UserPolicy::class
]; ];
/** /**

View File

@ -50,6 +50,11 @@ class User extends Authenticatable
: $user); : $user);
} }
public function albums()
{
return $this->hasMany(Album::class);
}
public function groups() public function groups()
{ {
return $this->belongsToMany(Group::class, 'user_groups'); return $this->belongsToMany(Group::class, 'user_groups');

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddUserProfileColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table)
{
$table->boolean('enable_profile_page')->default(false);
$table->string('profile_alias')->nullable(true);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table)
{
$table->dropColumn('enable_profile_page');
$table->dropColumn('profile_alias');
});
}
}

View File

@ -1,3 +1,16 @@
.activity-grid {
font-size: smaller;
}
.activity-grid th,td {
padding: 5px !important;
text-align: center;
}
.activity-grid td {
color: #fff;
}
.album-slideshow-container #image-preview { .album-slideshow-container #image-preview {
height: 600px; height: 600px;
max-width: 100%; max-width: 100%;

View File

@ -210,7 +210,8 @@ return [
'analytics_enable_visitor_hits_description' => 'Visitor hits to the public gallery will be recorded in the Blue Twilight database, allowing for analysis such as the most popular album/photo.', 'analytics_enable_visitor_hits_description' => 'Visitor hits to the public gallery will be recorded in the Blue Twilight database, allowing for analysis such as the most popular album/photo.',
'analytics_tab' => 'Analytics', 'analytics_tab' => 'Analytics',
'security_allow_self_registration' => 'Allow self-registration', 'security_allow_self_registration' => 'Allow self-registration',
'security_allow_self_registration_description' => 'With this option enabled, users can sign up for their own accounts. You can grant permissions to accounts to allow users to upload their own photos or manage yours.' 'security_allow_self_registration_description' => 'With this option enabled, users can sign up for their own accounts. You can grant permissions to accounts to allow users to upload their own photos or manage yours.',
'social_tab' => 'Social'
], ],
'select_all_action' => 'Select all', 'select_all_action' => 'Select all',
'select_all_album_active' => 'Any action you select in the list below will apply to all photos in this album.', 'select_all_album_active' => 'Any action you select in the list below will apply to all photos in this album.',

View File

@ -45,6 +45,8 @@ return [
'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',
'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.', 'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.',
'settings_social_user_profiles' => 'Enable public user profiles',
'settings_social_user_profiles_help' => 'Display public pages for users showing their albums, cameras used and activity.',
'storage_access_key_label' => 'Access key:', 'storage_access_key_label' => 'Access key:',
'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.', 'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
'storage_api_key_label' => 'API key:', 'storage_api_key_label' => 'API key:',

View File

@ -64,5 +64,15 @@ return [
], ],
'title' => 'Statistics', 'title' => 'Statistics',
'uploaded_12_months' => 'Photos uploaded in the last 12 months', 'uploaded_12_months' => 'Photos uploaded in the last 12 months',
],
'user_profile' => [
'activity' => 'Activity',
'activity_summary' => ':count photo on :date|:count photos on :date',
'activity_taken_p1' => 'Photos taken by :user_name:',
'activity_uploaded_p1' => 'Photos uploaded by :user_name:',
'albums' => 'Albums by :user_name',
'cameras' => 'Cameras',
'no_albums_p1' => 'No Photo Albums',
'no_albums_p2' => ':user_name has not created any albums yet.'
] ]
]; ];

View File

@ -23,6 +23,7 @@
@include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'email', 'tab_icon' => 'envelope', 'tab_text' => trans('admin.settings_email_tab')]) @include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'email', 'tab_icon' => 'envelope', 'tab_text' => trans('admin.settings_email_tab')])
@include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'security', 'tab_icon' => 'lock', 'tab_text' => trans('admin.settings_security_tab')]) @include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'security', 'tab_icon' => 'lock', 'tab_text' => trans('admin.settings_security_tab')])
@include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'analytics', 'tab_icon' => 'line-chart', 'tab_text' => trans('admin.settings.analytics_tab')]) @include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'analytics', 'tab_icon' => 'line-chart', 'tab_text' => trans('admin.settings.analytics_tab')])
@include(Theme::viewName('partials.tab'), ['active_tab' => 'general', 'tab_name' => 'social', 'tab_icon' => 'users', 'tab_text' => trans('admin.settings.social_tab')])
</ul> </ul>
{{-- Tab panes --}} {{-- Tab panes --}}
@ -313,6 +314,17 @@
<textarea class="form-control" rows="10" name="analytics_code">{{ old('analytics_code', $config['analytics_code']) }}</textarea> <textarea class="form-control" rows="10" name="analytics_code">{{ old('analytics_code', $config['analytics_code']) }}</textarea>
</fieldset> </fieldset>
</div> </div>
{{-- Social --}}
<div role="tabpanel" class="tab-pane" id="social-tab">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="social-user-profiles" name="social_user_profiles" @if (old('social_user_profiles', UserConfig::get('social_user_profiles')))checked="checked"@endif>
<label class="form-check-label" for="social-user-profiles">
<strong>@lang('forms.settings_social_user_profiles')</strong><br/>
@lang('forms.settings_social_user_profiles_help')
</label>
</div>
</div>
</div> </div>
<div class="pull-right" style="margin-top: 15px;"> <div class="pull-right" style="margin-top: 15px;">

View File

@ -0,0 +1,95 @@
@extends(Theme::viewName('layout'))
@section('title', $user->name)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('home') }}"><i class="fa fa-fw fa-home"></i></a></li>
<li class="breadcrumb-item active">{{ $user->name }}</li>
@endsection
@section('content')
<div class="container">
<div class="row">
<div id="user-avatar" class="col-md-2">
<img class="rounded" src="{{ Theme::gravatarUrl($user->email, 160) }}" title="{{ $user->name }}" />
</div>
<div class="col-md-10">
<h1>{{ $user->name }}</h1>
@if (!empty($user->profile_alias))
<h2 class="text-muted">{{ $user->profile_alias }}</h2>
@endif
</div>
</div>
<div class="row mt-5">
<div class="col">
@if (count($albums) == 0)
<h4 class="text-danger"><b>@lang('gallery.user_profile.no_albums_p1')</b></h4>
<p>@lang('gallery.user_profile.no_albums_p2', ['user_name' => $user->name])</p>
@else
<h3 class="mb-4 text-muted">@lang('gallery.user_profile.albums', ['user_name' => $user->name])</h3>
<div class="row">
@foreach ($albums as $album)
<div class="col-sm-4 col-md-3 text-left" style="max-width: 250px;">
<div class="card mb-3">
<img class="card-img-top" src="{{ $album->thumbnailUrl('preview') }}" style="max-height: 120px;"/>
<div class="card-body">
<h5 class="card-title"><a href="{{ $album->url() }}">{{ $album->name }}</a></h5>
</div>
<div class="card-footer">
<small class="text-muted">
<i class="fa fa-fw fa-photo"></i> {{ number_format($album->photos_count) }} {{ trans_choice('gallery.photos', $album->photos_count) }}
@if ($album->children_count > 0)
<i class="fa fa-fw fa-book ml-3"></i> {{ number_format($album->children_count) }} {{ trans_choice('gallery.child_albums', $album->children_count) }}
@endif
</small>
</div>
</div>
</div>
@endforeach
</div>
@endif
<h3 class="mt-5 text-muted">@lang('gallery.user_profile.activity')</h3>
<p>@lang('gallery.user_profile.activity_taken_p1', ['user_name' => $user->name])</p>
@include (Theme::viewName('partials.user_profile_activity_grid'), ['activity' => $activity_taken])
@if (count($cameras) > 0)
<h3 class="mb-4 mt-5 text-muted">@lang('gallery.user_profile.cameras')</h3>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>@lang('admin.album_camera_make')</th>
<th>@lang('admin.album_camera_model')</th>
<th>@lang('admin.album_camera_software')</th>
<th>@lang('admin.album_camera_photo_count')</th>
</tr>
</thead>
<tbody>
@foreach ($cameras as $camera)
<tr>
<td>{{ $camera->camera_make }}</td>
<td>{{ $camera->camera_model }}</td>
<td>{{ $camera->camera_software }}</td>
<td>{{ $camera->photo_count }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
</div>
@endsection
@push ('scripts')
<script type="text/javascript">
$(function () {
$('[data-toggle="tooltip"]').tooltip()
});
</script>
@endpush

View File

@ -0,0 +1,30 @@
<table class="table activity-grid">
<thead>
<tr>
<th></th>
@foreach ($activity as $year => $months)
@foreach ($months as $month => $dates)
<th style="vertical-align: top;">{{ $month }}@if ($month == 'Jan')<br/>{{ $year }}@endif</th>
@endforeach
@endforeach
</tr>
</thead>
<tbody>
@for ($i = 1; $i <= 31; $i++)
<tr>
<th>{{ $i }}</th>
@foreach ($activity as $year => $months)
@foreach ($months as $month => $dates)
@if (isset($dates[$i]) && $dates[$i] > 0)
<td class="bg-primary" data-toggle="tooltip" data-placement="top" title="{{ trans_choice('gallery.user_profile.activity_summary', $dates[$i], ['count' => $dates[$i], 'date' => sprintf('%d %s %d', $i, $month, $year)]) }}">{{ $dates[$i] }}</td>
@elseif (isset($dates[$i]) && $dates[$i] == 0)
<td>&nbsp;</td>
@else
<td class="bg-light">&nbsp;</td>
@endif
@endforeach
@endforeach
</tr>
@endfor
</tbody>
</table>

View File

@ -103,4 +103,7 @@ Route::get('i/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@downloa
->where('albumUrlAlias', '.*'); ->where('albumUrlAlias', '.*');
Route::get('label/{labelAlias}', 'Gallery\LabelController@show') Route::get('label/{labelAlias}', 'Gallery\LabelController@show')
->name('viewLabel') ->name('viewLabel')
->where('labelAlias', '.*'); ->where('labelAlias', '.*');
Route::get('user/{idOrAlias}', 'Gallery\UserController@show')
->name('viewUser')
->where('idOrAlias', '.*');