Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡ sort by real departure and query optimization #2703

Closed
wants to merge 11 commits into from
148 changes: 81 additions & 67 deletions app/Http/Controllers/Backend/User/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,103 @@
use App\Http\Controllers\Controller;
use App\Models\Status;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;

abstract class DashboardController extends Controller
{

private static function getGenericQuery(User $user): Builder {
$query = Status::with([
'event',
'likes',
'user.blockedByUsers',
'user.blockedUsers',
'checkin',
'tags',
'mentions.mentioned',
'checkin.originStopover.station',
'checkin.destinationStopover.station',
'checkin.trip.stopovers.station'
])
->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id')
->join('train_stopovers AS origin_stopover', 'train_checkins.origin_stopover_id', '=', 'origin_stopover.id')
->join('users', 'statuses.user_id', '=', 'users.id')
->where('origin_stopover.departure_real', '<', now()->addMinutes(20))
->where('origin_stopover.departure_real', '>', now()->subDays(2)) //TODO: discuss - dashboard should show statuses from the last 2 days. This is a performance dealbreaker
->select('statuses.*')
->orderByDesc('origin_stopover.departure_real'); // TODO: manual_departure

// left join follows to check if user follows the status author (checked in caller function)
$query->leftJoin('follows', function($join) use ($user) {
$join->on('follows.follow_id', '=', 'users.id')
->where('follows.user_id', '=', $user->id);
});

return $query;
}

public static function getPrivateDashboard(User $user): Paginator {
$followingIDs = $user->follows->pluck('id');
$followingIDs[] = $user->id;
return Status::with([
'event',
'likes',
'user.blockedByUsers',
'user.blockedUsers',
'checkin',
'tags',
'mentions.mentioned',
'checkin.originStopover.station',
'checkin.destinationStopover.station',
'checkin.trip.stopovers.station'
])
->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id')
->select('statuses.*')
->where('train_checkins.departure', '<', Carbon::now()->addMinutes(20))
->orderBy('train_checkins.departure', 'desc')
->whereIn('statuses.user_id', $followingIDs)
->whereNotIn('statuses.user_id', $user->mutedUsers->pluck('id'))
$query = self::getGenericQuery($user);

return $query->whereNotNull('follows.id')
->whereIn('statuses.visibility', [
StatusVisibility::PUBLIC->value,
StatusVisibility::FOLLOWERS->value,
StatusVisibility::AUTHENTICATED->value
])
->orWhere('statuses.user_id', $user->id)
->latest()
->simplePaginate(15);
}

public static function getGlobalDashboard(User $user): Paginator {
return Status::with([
'event',
'likes',
'user.blockedByUsers',
'user.blockedUsers',
'checkin',
'mentions.mentioned',
'tags',
'checkin.originStopover.station',
'checkin.destinationStopover.station',
'checkin.trip.stopovers.station'
])
->join('train_checkins', 'train_checkins.status_id', '=', 'statuses.id')
->join('users', 'statuses.user_id', '=', 'users.id')
->where(function(Builder $query) use ($user) {
//Visibility checks: One of the following options must be true

//Option 1: User is public AND status is public
$query->where(function(Builder $query) {
$query->where('users.private_profile', 0)
->whereIn('visibility', [
StatusVisibility::PUBLIC->value,
StatusVisibility::AUTHENTICATED->value
]);
});

//Option 2: Status is from oneself
$query->orWhere('users.id', $user->id);

//Option 3: Status is from a followed BUT not unlisted or private
$query->orWhere(function(Builder $query) use ($user) {
$query->whereIn('users.id', $user->follows()->select('follow_id'))
->whereNotIn('statuses.visibility', [
StatusVisibility::UNLISTED->value,
StatusVisibility::PRIVATE->value,
]);
});
})
->where('train_checkins.departure', '<', Carbon::now()->addMinutes(20))
->whereNotIn('statuses.user_id', $user->mutedUsers()->select('muted_id'))
->whereNotIn('statuses.user_id', $user->blockedUsers()->select('blocked_id'))
->whereNotIn('statuses.user_id', $user->blockedByUsers()->select('user_id'))
->select('statuses.*')
->orderByDesc('train_checkins.departure')
->simplePaginate(15);
$query = self::getGenericQuery($user);

// exclude muted users
$query->leftJoin('user_mutes', function($join) use ($user) {
$join->on('user_mutes.muted_id', '=', 'users.id')
->where('user_mutes.user_id', '=', $user->id);
})->whereNull('user_mutes.id');

// exclude blocked users
$query->leftJoin('user_blocks AS blocked_users', function($join) use ($user) {
$join->on('blocked_users.blocked_id', '=', 'users.id')
->where('blocked_users.user_id', '=', $user->id);
})->whereNull('blocked_users.id');

// exclude blocked by users
$query->leftJoin('user_blocks AS blocked_by_users', function($join) use ($user) {
$join->on('blocked_by_users.user_id', '=', 'users.id')
->where('blocked_by_users.blocked_id', '=', $user->id);
})->whereNull('blocked_by_users.id');

// only show statuses user is allowed to see
$query->where(function(Builder $query) use ($user) {
//Visibility checks: One of the following options must be true

//Option 1: User is public AND status is public
$query->where(function(Builder $query) {
$query->where('users.private_profile', 0)
->whereIn('visibility', [
StatusVisibility::PUBLIC->value,
StatusVisibility::AUTHENTICATED->value
]);
});

//Option 2: Status is from oneself
$query->orWhere('users.id', $user->id);

//Option 3: Status is from a followed BUT not unlisted or private
$query->orWhere(function(Builder $query) {
// see join above
$query->whereNotNull('follows.id')
->whereNotIn('statuses.visibility', [
StatusVisibility::UNLISTED->value,
StatusVisibility::PRIVATE->value,
]);
});
});

return $query->simplePaginate(15);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void {
Schema::table('train_checkins', static function(Blueprint $table) {
$table->index(['status_id', 'departure']);
});
}

public function down(): void {
Schema::table('train_checkins', static function(Blueprint $table) {
$table->dropIndex(['status_id', 'departure']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void {
Schema::table('train_stopovers', static function(Blueprint $table) {
$table->index(['id', 'departure_real', 'departure_planned']);
});
}

public function down(): void {
Schema::table('train_stopovers', static function(Blueprint $table) {
$table->dropIndex(['id', 'departure_real', 'departure_planned']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{

public function up(): void {
Schema::table('users', static function(Blueprint $table) {
$table->index(['id', 'private_profile']);
});
}

public function down(): void {
Schema::table('users', static function(Blueprint $table) {
$table->dropIndex(['id', 'private_profile']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void {
Schema::table('follows', static function(Blueprint $table) {
$table->index(['follow_id', 'user_id']);
});
}

public function down(): void {
Schema::table('follows', static function(Blueprint $table) {
$table->dropIndex(['follow_id', 'user_id']);
});
}
};
3 changes: 0 additions & 3 deletions database/seeders/UsersTableSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use App\Models\IcsToken;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UsersTableSeeder extends Seeder
{
Expand All @@ -23,7 +22,6 @@ public function run(): void {
'username' => 'Gertrud123',
'name' => 'Gertrud',
'email' => '[email protected]',
'password' => Hash::make('thisisnotasecurepassword123'),
]);
$gertrud->assignRole('admin');
$gertrud->assignRole('closed-beta');
Expand All @@ -37,7 +35,6 @@ public function run(): void {
'avatar' => null, // no avatar
'email' => '[email protected]',
'private_profile' => true,
'password' => Hash::make('thisisnotasecurepassword123')
]);
}

Expand Down
5 changes: 5 additions & 0 deletions docs/contributing/dev-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ php artisan passport:install
Use your webserver of choice or the in php included dev server (`php artisan serve`) to boot the application.
You should see the Träwelling homepage at http://localhost:8000.

If you have seeded the database, you can log in using the following credentials:

- Username: `Gertrud123` or `bob`
- Password: `password`

Additionally, for continuous functionality:

- Create a cron job to run `php artisan schedule:run` every minute.
Expand Down
Loading