implement GRIDs

pull/26/head
Ondřej Hruška 6 years ago
parent 8988df93fd
commit 755c65a42d
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 4
      app/Console/Commands/ConfirmUser.php
  2. 13
      app/Http/Controllers/TableController.php
  3. 21
      app/Http/Kernel.php
  4. 19
      app/Http/Middleware/ThrottleIfNotDebug.php
  5. 37
      app/Models/Row.php
  6. 6
      app/Models/User.php
  7. 4
      app/Tables/Column.php
  8. 54
      database/migrations/2018_08_01_204822_add_row_sequence_generator.php
  9. 28
      database/migrations/2018_08_01_213353_make_grid_column_mandatory_in_rows_table.php
  10. 1
      porklib/Utils/Utils.php
  11. 25
      resources/views/table/_action-buttons.blade.php
  12. 6
      resources/views/table/_rows.blade.php

@ -39,7 +39,9 @@ class ConfirmUser extends Command
public function handle() public function handle()
{ {
$u = User::resolve($this->argument('user')); $u = User::resolve($this->argument('user'));
$u->update('confirmed', true); $u->update(['confirmed' => true]);
$this->info("User #$u->id with e-mail $u->email and handle @$u->name was confirmed."); $this->info("User #$u->id with e-mail $u->email and handle @$u->name was confirmed.");
dd($u);
} }
} }

@ -189,12 +189,19 @@ class TableController extends Controller
// Preparing data to insert into the Rows table // Preparing data to insert into the Rows table
$rowsData = null; $rowsData = null;
try { try {
$rowsData = array_map(function ($row) use ($columns) { $grid_range = Row::allocateGRIDs(count($rowTable));
if (count($row) == 0 || count($row)==1&&$row[0]=='') return null; $grid_cnt = $grid_range[0];
$rowsData = array_map(function ($row) use ($columns, &$grid_cnt) {
if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null;
if (count($row) != count($columns)) { if (count($row) != count($columns)) {
throw new NotApplicableException("All rows must have " . count($columns) . " fields."); throw new NotApplicableException("All rows must have " . count($columns) . " fields.");
} }
$parsed = [];
$parsed = [
'_grid' => $grid_cnt++
];
foreach ($row as $i => $val) { foreach ($row as $i => $val) {
$key = $columns[$i]->name; $key = $columns[$i]->name;
if (strlen($val) > 255) { if (strlen($val) > 255) {

@ -2,6 +2,13 @@
namespace App\Http; namespace App\Http;
use App\Http\Middleware\ActivatedAccount;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\ThrottleIfNotDebug;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel class Kernel extends HttpKernel
@ -16,9 +23,9 @@ class Kernel extends HttpKernel
protected $middleware = [ protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class, TrustProxies::class,
]; ];
/** /**
@ -29,12 +36,12 @@ class Kernel extends HttpKernel
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
'throttle:120,15', // try to prevent people refresh-spamming the server to game table visit counts 'throttle:120,15', // try to prevent people refresh-spamming the server to game table visit counts
\App\Http\Middleware\EncryptCookies::class, EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, VerifyCsrfToken::class,
'bindings', 'bindings',
], ],
@ -52,14 +59,14 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'activated' => \App\Http\Middleware\ActivatedAccount::class, 'activated' => ActivatedAccount::class,
'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => ThrottleIfNotDebug::class,
]; ];
} }

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottleIfNotDebug extends ThrottleRequests
{
public function handle($request, \Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
if (config('app.debug')) {
return $next($request);
}
return parent::handle($request, $next, $maxAttempts, $decayMinutes);
}
}

@ -6,7 +6,7 @@ namespace App\Models;
* Row in a data table * Row in a data table
* *
* @property int $id * @property int $id
* @property string $data - JSONB * @property string $data - JSONB, always containing _grid
*/ */
class Row extends BaseModel class Row extends BaseModel
{ {
@ -16,4 +16,39 @@ class Row extends BaseModel
protected $guarded = []; protected $guarded = [];
public $timestamps = false; public $timestamps = false;
public function getGridAttribute() {
return $this->data->_grid;
}
public function setGridAttribute($value) {
$this->data->_grid = $value;
}
/**
* Allocate a single Global Row ID, application-unique.
*
* GRIDs are used to uniquely identify existing or proposed new rows,
* and are preserved after row modifications, to ensure change proposals have
* a clear target.
*
* @return int
*/
public static function allocateGRID()
{
return \DB::selectOne("SELECT nextval('global_row_id_seq') AS grid;")->grid;
}
/**
* Allocate a block of Global Row IDs, application-unique.
*
* @see Row::allocateGRID()
*
* @return int[] first and last
*/
public static function allocateGRIDs($count)
{
$last = \DB::selectOne("SELECT multi_nextval('global_row_id_seq', ?) AS last_grid;", [$count])->last_grid;
return [$last - $count + 1, $last];
}
} }

@ -62,7 +62,7 @@ class User extends BaseModel implements
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'title', 'email', 'password', 'bio', 'website' 'name', 'title', 'email', 'password', 'bio', 'website', 'confirmed'
]; ];
/** /**
@ -183,6 +183,10 @@ class User extends BaseModel implements
} }
else { else {
$u = static::where('email', $ident)->first(); $u = static::where('email', $ident)->first();
if (!$u) {
$u = static::where('name', $ident)->first();
}
} }
if (!$u) throw new NotExistException("No user found matching \"$ident\""); if (!$u) throw new NotExistException("No user found matching \"$ident\"");

@ -57,6 +57,10 @@ class Column implements JsonSerializable
$this->title = $b->title; $this->title = $b->title;
$this->type = $b->type; $this->type = $b->type;
if ($this->name == '_grid') { // global row ID
throw new NotApplicableException("_grid is a reserved column name.");
}
if (!in_array($this->type, self::colTypes)) { if (!in_array($this->type, self::colTypes)) {
throw new NotApplicableException("\"$this->type\" is not a valid column type."); throw new NotApplicableException("\"$this->type\" is not a valid column type.");
} }

@ -0,0 +1,54 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRowSequenceGenerator extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// https://www.depesz.com/2008/03/20/getting-multiple-values-from-sequences/
DB::unprepared(/** @lang PostgreSQL */
<<<PGSQL
CREATE OR REPLACE FUNCTION multi_nextval(use_seqname TEXT, use_increment INT4) RETURNS INT4 AS $$
DECLARE
reply int4;
BEGIN
perform pg_advisory_lock(20180801);
execute 'ALTER SEQUENCE ' || quote_ident(use_seqname) || ' INCREMENT BY ' || use_increment::text;
reply := nextval(use_seqname);
execute 'ALTER SEQUENCE ' || quote_ident(use_seqname) || ' INCREMENT BY 1';
perform pg_advisory_unlock(20180801);
return reply;
END;
$$ LANGUAGE 'plpgsql';
PGSQL
);
DB::unprepared('CREATE SEQUENCE global_row_id_seq START 0 MINVALUE 0;');
// We have to increment manually once before the above function can be used - that is because
// nextval will return the initial value the first time it's called, so it would not advance by
// the given step at all. This would give us negative values - not a problem in postgres without unsigned
// types, but it loooks bad.
DB::select("SELECT nextval('global_row_id_seq') as nv;");
// The multi_nextval() is concurrency-safe, except it can sometimes happen that
// nextval in other connection also increments by the bigger step. The only downside is "wasted IDs".
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::unprepared('DROP FUNCTION IF EXISTS multi_nextval(TEXT, INT4);');
DB::unprepared('DROP SEQUENCE IF EXISTS global_row_id_seq;');
}
}

@ -0,0 +1,28 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MakeGridColumnMandatoryInRowsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::unprepared("ALTER TABLE rows ADD CONSTRAINT grid_must_exist CHECK (data ? '_grid');");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::unprepared("ALTER TABLE rows DROP CONSTRAINT grid_must_exist;");
}
}

@ -557,6 +557,7 @@ class Utils
public static function logQueries() public static function logQueries()
{ {
\DB::listen(function ($query) { \DB::listen(function ($query) {
$b = [];
/** @var QueryExecuted $query */ /** @var QueryExecuted $query */
foreach ($query->bindings as $i=>$binding) $b[$i] = "'$binding'"; foreach ($query->bindings as $i=>$binding) $b[$i] = "'$binding'";
Log::debug('SQL: ' . preg_replace_array('/\\?/', $b, $query->sql)); Log::debug('SQL: ' . preg_replace_array('/\\?/', $b, $query->sql));

@ -10,25 +10,26 @@
@sr(Table actions) @sr(Table actions)
{{-- Disabled until implemented --}} {{-- Disabled until implemented --}}
@if(true)
@if(guest() || !user()->confirmed || user()->ownsTable($table)) @if(guest() || !user()->confirmed || user()->ownsTable($table))
{{-- Guest, unconfirmed, or a table owner --}}
{{-- Passive fork buttons with counter --}} {{-- Passive fork buttons with counter --}}
<a href="" class="btn btn-outline-primary py-1 btn-sm" title="Forks" <a href="" class="btn btn-outline-primary py-1 btn-sm" title="Forks"
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
{{ $table->forks_count ?: '–' }}&nbsp; @icon(fa-code-fork, sr:Forks)&nbsp;
@icon(fa-code-fork, sr:Forks) {{ $table->forks_count ?: '–' }}
</a> </a>
{{-- Passive favourite buttons with counter --}} {{-- Passive favourite buttons with counter --}}
<a href="" class="btn btn-outline-primary py-1 btn-sm" <a href="" class="btn btn-outline-primary py-1 btn-sm"
title="Favourites" data-toggle="tooltip" data-placement="top"> title="Favourites" data-toggle="tooltip" data-placement="top">
{{ $table->favourites_count ?: '–' }}&nbsp; @icon(fa-star, sr:Favourites)&nbsp;
@icon(fa-star, sr:Favourites) {{ $table->favourites_count ?: '–' }}
</a> </a>
@else @else
{{-- Logged in and does not own the table --}}
{{-- Active fork button | counter --}} {{-- Active fork button | counter --}}
<div class="btn-group" role="group" aria-label="Fork"> <div class="btn-group" role="group" aria-label="Fork">
@ -55,7 +56,6 @@
@icon(fa-star-o, sr:Favourite) @icon(fa-star-o, sr:Favourite)
</a> </a>
@endif @endif
<a href="" class="btn btn-outline-primary py-1 btn-sm" title="Favourite Count" <a href="" class="btn btn-outline-primary py-1 btn-sm" title="Favourite Count"
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
{{ $table->favourites_count ?: '–' }} {{ $table->favourites_count ?: '–' }}
@ -67,8 +67,8 @@
{{-- Comments button with counter --}} {{-- Comments button with counter --}}
<a href="" class="btn btn-outline-primary py-1 btn-sm" title="Comments" <a href="" class="btn btn-outline-primary py-1 btn-sm" title="Comments"
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
{{ $table->comments_count ?: '–' }}&nbsp; @icon(fa-comment, sr:Comments)&nbsp;
@icon(fa-comment, sr:Comments) {{ $table->comments_count ?: '–' }}
</a> </a>
{{-- Active proposals button | counter --}} {{-- Active proposals button | counter --}}
@ -77,22 +77,25 @@
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
@icon(fa-inbox fa-pr, sr:Change Proposals){{ $table->proposals_count ?: '–' }} @icon(fa-inbox fa-pr, sr:Change Proposals){{ $table->proposals_count ?: '–' }}
</a> </a>
@auth
@if(user()->ownsTable($table)) @if(user()->ownsTable($table))
{{-- Table owner logged in --}}
<a href="" class="btn btn-outline-primary py-1 btn-sm btn-square" title="Draft Change" <a href="" class="btn btn-outline-primary py-1 btn-sm btn-square" title="Draft Change"
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
@icon(fa-pencil, sr:Draft Change) @icon(fa-pencil, sr:Draft Change)
</a> </a>
@else @else
{{-- Not a table owner --}}
<a href="" class="btn btn-outline-primary py-1 btn-sm btn-square" title="Propose Change" <a href="" class="btn btn-outline-primary py-1 btn-sm btn-square" title="Propose Change"
data-toggle="tooltip" data-placement="top"> data-toggle="tooltip" data-placement="top">
@icon(fa-pencil, sr:Propose Change) @icon(fa-pencil, sr:Propose Change)
</a> </a>
@endif @endif
@endauth
</div> </div>
@endif @if(authed() && user()->ownsTable($table))
{{-- Table opts menu for table owner --}}
@if(user() && user()->ownsTable($table))
<a href="{{ $table->settingsRoute }}" class="btn btn-outline-primary py-1 btn-sm" <a href="{{ $table->settingsRoute }}" class="btn btn-outline-primary py-1 btn-sm"
title="Table Options" data-toggle="tooltip" data-placement="top"> title="Table Options" data-toggle="tooltip" data-placement="top">
@icon(fa-wrench, sr:Table Options) @icon(fa-wrench, sr:Table Options)

@ -4,12 +4,13 @@
@php @php
/** @var object[] $columns */ /** @var object[] $columns */
/** @var \App\Models\Row[]|array[][] $rows */ /** @var \App\Models\Row[] $rows */
@endphp @endphp
<table class="table table-hover table-sm"> <table class="table table-hover table-sm">
<thead> <thead>
<tr> <tr>
<th>#GRID</th>
@foreach($columns as $col) @foreach($columns as $col)
<th>{{ $col->title }}</th> <th>{{ $col->title }}</th>
@endforeach @endforeach
@ -18,8 +19,9 @@
<tbody> <tbody>
@foreach($rows as $row) @foreach($rows as $row)
<tr> <tr>
<td>{{ $row->data->_grid }}</td>
@foreach($columns as $col) @foreach($columns as $col)
<td data-id="{{ $row->id }}">{{ $row['data']->{$col->name} }}</td> <td data-id="{{ $row->id }}">{{ $row->data->{$col->name} }}</td>
@endforeach @endforeach
</tr> </tr>
@endforeach @endforeach

Loading…
Cancel
Save