add column unique numbering scheme, more efficient selects
This commit is contained in:
@@ -41,7 +41,5 @@ class ConfirmUser extends Command
|
|||||||
$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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Revision;
|
use App\Models\Revision;
|
||||||
use App\Models\Row;
|
|
||||||
use App\Models\Table;
|
use App\Models\Table;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Tables\Column;
|
use App\Tables\Column;
|
||||||
|
use App\Tables\ColumnNumerator;
|
||||||
use App\Tables\CStructArrayExporter;
|
use App\Tables\CStructArrayExporter;
|
||||||
use App\Tables\CsvExporter;
|
use App\Tables\CsvExporter;
|
||||||
use App\Tables\CXMacroExporter;
|
use App\Tables\CXMacroExporter;
|
||||||
use App\Tables\JsonExporter;
|
use App\Tables\JsonExporter;
|
||||||
use App\Tables\PhpExporter;
|
use App\Tables\PhpExporter;
|
||||||
|
use App\Tables\RowNumerator;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use MightyPork\Exceptions\NotApplicableException;
|
use MightyPork\Exceptions\NotApplicableException;
|
||||||
@@ -41,11 +42,15 @@ class TableController extends Controller
|
|||||||
|
|
||||||
$this->countTableVisit($request, $tableModel);
|
$this->countTableVisit($request, $tableModel);
|
||||||
|
|
||||||
|
$columns = Column::columnsFromJson($revision->columns);
|
||||||
|
|
||||||
|
$rows = $revision->rowsData($columns)->paginate(25, []);
|
||||||
|
|
||||||
return view('table.view', [
|
return view('table.view', [
|
||||||
'table' => $tableModel,
|
'table' => $tableModel,
|
||||||
'revision' => $revision,
|
'revision' => $revision,
|
||||||
'columns' => Column::columnsFromJson($revision->columns),
|
'columns' => $columns,
|
||||||
'rows' => $revision->rows()->paginate(25),
|
'rows' => $rows,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +83,7 @@ class TableController extends Controller
|
|||||||
"Patella vulgata,common limpet,20";
|
"Patella vulgata,common limpet,20";
|
||||||
|
|
||||||
$columns = Column::columnsFromJson([
|
$columns = Column::columnsFromJson([
|
||||||
|
// fake 'id' to satisfy the check in Column constructor
|
||||||
['name' => 'latin', 'type' => 'string', 'title' => 'Latin Name'],
|
['name' => 'latin', 'type' => 'string', 'title' => 'Latin Name'],
|
||||||
['name' => 'common', 'type' => 'string', 'title' => 'Common Name'],
|
['name' => 'common', 'type' => 'string', 'title' => 'Common Name'],
|
||||||
['name' => 'lifespan', 'type' => 'int', 'title' => 'Lifespan (years)']
|
['name' => 'lifespan', 'type' => 'int', 'title' => 'Lifespan (years)']
|
||||||
@@ -90,6 +96,9 @@ class TableController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show table settings edit form
|
||||||
|
*/
|
||||||
public function settings(Request $request, User $user, string $table)
|
public function settings(Request $request, User $user, string $table)
|
||||||
{
|
{
|
||||||
/** @var Table $tableModel */
|
/** @var Table $tableModel */
|
||||||
@@ -102,6 +111,9 @@ class TableController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store modified table settings
|
||||||
|
*/
|
||||||
public function storeSettings(Request $request, User $user, string $table)
|
public function storeSettings(Request $request, User $user, string $table)
|
||||||
{
|
{
|
||||||
/** @var Table $tableModel */
|
/** @var Table $tableModel */
|
||||||
@@ -149,85 +161,100 @@ class TableController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if table name is unique for user
|
// Check if table name is unique for user
|
||||||
if ($u->tables()->where('name', $input->name)->exists()) {
|
if ($u->hasTable($input->name)) {
|
||||||
return $this->backWithErrors([
|
return $this->backWithErrors([
|
||||||
'name' => "A table called \"$input->name\" already exists in your account.",
|
'name' => "A table called \"$input->name\" already exists in your account.",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- COLUMNS ---
|
||||||
// Parse and validate the columns specification
|
// Parse and validate the columns specification
|
||||||
/** @var Column[] $columns */
|
/** @var Column[] $columns */
|
||||||
$columns = [];
|
$columns = [];
|
||||||
$column_keys = []; // for checking duplicates
|
$col_names = []; // for checking duplicates
|
||||||
$colsArray = fromJSON($input->columns);
|
$colsArray = fromJSON($input->columns);
|
||||||
|
|
||||||
// prevent griefing via long list of columns
|
// prevent griefing via long list of columns
|
||||||
if (count($colsArray) > 100) return $this->backWithErrors(['columns' => "Too many columns"]);
|
if (count($colsArray) > 100) return $this->backWithErrors(['columns' => "Too many columns"]);
|
||||||
|
|
||||||
foreach ($colsArray as $col) {
|
foreach ($colsArray as $colObj) {
|
||||||
if (!isset($col->name) || !isset($col->type) || empty($col->name) || empty($col->type)) {
|
// ensure column has a title
|
||||||
return $this->backWithErrors(['columns' => "All columns must have at least name and type."]);
|
if (!isset($colObj->title)) {
|
||||||
|
$colObj->title = $colObj->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (in_array($col->name, $column_keys)) {
|
if (in_array($colObj->name, $col_names)) {
|
||||||
return $this->backWithErrors(['columns' => "Duplicate column: $col->name"]);
|
return $this->backWithErrors(['columns' => "Duplicate column: $colObj->name"]);
|
||||||
}
|
}
|
||||||
$column_keys[] = $col->name;
|
|
||||||
|
|
||||||
if (!isset($col->title)) $col->title = $col->name;
|
$col_names[] = $colObj->name;
|
||||||
$columns[] = new Column($col);
|
$columns[] = new Column($colObj);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
// validation errors from the Column constructor
|
||||||
return $this->backWithErrors(['columns' => $e->getMessage()]);
|
return $this->backWithErrors(['columns' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]);
|
if (count($columns) == 0) {
|
||||||
|
return $this->backWithErrors(['columns' => "Define at least one column"]);
|
||||||
|
}
|
||||||
|
|
||||||
$rowTable = array_map('str_getcsv', explode("\n", $input->data));
|
// Now assign column IDs
|
||||||
|
$columnNumerator = new ColumnNumerator(count($columns));
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$column->setID($columnNumerator->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DATA ---
|
||||||
|
$dataCsvLines = array_map('str_getcsv', explode("\n", $input->data));
|
||||||
|
|
||||||
// Preparing data to insert into the Rows table
|
// Preparing data to insert into the Rows table
|
||||||
$rowsData = null;
|
$rowsToInsert = null;
|
||||||
|
$rowNumerator = null;
|
||||||
try {
|
try {
|
||||||
$grid_range = Row::allocateGRIDs(count($rowTable));
|
$rowsToInsert = collect($dataCsvLines)->map(function ($row) use ($columns) {
|
||||||
$grid_cnt = $grid_range[0];
|
if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null; // discard empty lines
|
||||||
|
|
||||||
$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 = [
|
$data = [];
|
||||||
'_grid' => $grid_cnt++
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($row as $i => $val) {
|
foreach ($row as $i => $val) {
|
||||||
$key = $columns[$i]->name;
|
$col = $columns[$i];
|
||||||
if (strlen($val) > 255) {
|
|
||||||
// try to stop people inserting unstructured crap / malformed CSV
|
|
||||||
throw new NotApplicableException("Value for column $key too long.");
|
|
||||||
}
|
|
||||||
$parsed[$key] = $columns[$i]->cast($val);
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'data' => $parsed,
|
|
||||||
];
|
|
||||||
}, $rowTable);
|
|
||||||
|
|
||||||
$rowsData = array_filter($rowsData);
|
if (strlen($val) > 1000) {
|
||||||
|
// try to stop people inserting unstructured crap / malformed CSV
|
||||||
|
throw new NotApplicableException("Value for column {$col->name} too long.");
|
||||||
|
}
|
||||||
|
$data[$col->id] = $col->cast($val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return in a format prepared for filling eloquent
|
||||||
|
return ['data' => $data];
|
||||||
|
})->filter()->all(); // remove nulls, to array
|
||||||
|
|
||||||
|
// attach _id labels to all rows
|
||||||
|
$rowNumerator = new RowNumerator(count($dataCsvLines));
|
||||||
|
foreach ($rowsToInsert as &$item) {
|
||||||
|
$item['data']['_id'] = $rowNumerator->next();
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->backWithErrors(['data' => $e->getMessage()]);
|
return $this->backWithErrors(['data' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- STORE TO DB ---
|
||||||
|
|
||||||
|
/* Fields for the new Revision instance */
|
||||||
$revisionFields = [
|
$revisionFields = [
|
||||||
'note' => "Initial revision of table $u->name/$input->name",
|
'note' => "Initial revision of table $u->name/$input->name",
|
||||||
'columns' => $columns,
|
'columns' => $columns,
|
||||||
'row_count' => count($rowsData),
|
'row_count' => count($rowsToInsert),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* Fields for the new Table instance */
|
||||||
$tableFields = [
|
$tableFields = [
|
||||||
'owner_id' => $u->id,
|
'owner_id' => $u->id,
|
||||||
'revision_id' => 0,
|
|
||||||
'name' => $input->name,
|
'name' => $input->name,
|
||||||
'title' => $input->title,
|
'title' => $input->title,
|
||||||
'description' => $input->description,
|
'description' => $input->description,
|
||||||
@@ -235,10 +262,12 @@ class TableController extends Controller
|
|||||||
'origin' => $input->origin,
|
'origin' => $input->origin,
|
||||||
];
|
];
|
||||||
|
|
||||||
\DB::transaction(function () use ($revisionFields, $tableFields, $rowsData) {
|
/** @var Table $table */
|
||||||
|
$table = null;
|
||||||
|
\DB::transaction(function () use ($revisionFields, $tableFields, $rowsToInsert, &$table) {
|
||||||
$revision = Revision::create($revisionFields);
|
$revision = Revision::create($revisionFields);
|
||||||
|
|
||||||
$tableFields['revision_id'] = $revision->id; // to meet the not-null constraint
|
$tableFields['revision_id'] = $revision->id; // current revision (not-null constraint on this FK)
|
||||||
$table = Table::create($tableFields);
|
$table = Table::create($tableFields);
|
||||||
|
|
||||||
// Attach the revision to the table, set as current
|
// Attach the revision to the table, set as current
|
||||||
@@ -246,10 +275,10 @@ class TableController extends Controller
|
|||||||
$table->revision()->associate($revision);
|
$table->revision()->associate($revision);
|
||||||
|
|
||||||
// Spawn rows, linked to the revision
|
// Spawn rows, linked to the revision
|
||||||
$revision->rows()->createMany($rowsData);
|
$revision->rows()->createMany($rowsToInsert);
|
||||||
});
|
});
|
||||||
|
|
||||||
return redirect(route('table.view', ['user' => $u, 'table' => $input->name]));
|
return redirect($table->viewRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,7 +289,7 @@ class TableController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function countTableVisit(Request $request, Table $table)
|
private function countTableVisit(Request $request, Table $table)
|
||||||
{
|
{
|
||||||
$cookieName = "view_$table->id";
|
$cookieName = "view_{$table->owner->name}_{$table->name}";
|
||||||
if (!$request->cookie($cookieName, false)) {
|
if (!$request->cookie($cookieName, false)) {
|
||||||
$ua = $request->userAgent();
|
$ua = $request->userAgent();
|
||||||
// Filter out suspicious user agents
|
// Filter out suspicious user agents
|
||||||
@@ -271,6 +300,13 @@ class TableController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple export via a preset
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param User $user
|
||||||
|
* @param string $table
|
||||||
|
*/
|
||||||
public function export(Request $request, User $user, string $table)
|
public function export(Request $request, User $user, string $table)
|
||||||
{
|
{
|
||||||
/** @var Table $tableModel */
|
/** @var Table $tableModel */
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class BaseModel extends Model
|
|||||||
{
|
{
|
||||||
public function getAttribute($key)
|
public function getAttribute($key)
|
||||||
{
|
{
|
||||||
if (! $key) {
|
if ($this->exists && ! $key) {
|
||||||
throw new \LogicException("No attribute ".var_export($key, true));
|
throw new \LogicException("No attribute ".var_export($key, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class BaseModel extends Model
|
|||||||
*/
|
*/
|
||||||
public function getRelationValue($key)
|
public function getRelationValue($key)
|
||||||
{
|
{
|
||||||
if (!method_exists($this, $key)) {
|
if ($this->exists && !method_exists($this, $key)) {
|
||||||
throw new \LogicException("No attribute or relation ".var_export($key, true));
|
throw new \LogicException("No attribute or relation ".var_export($key, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Tables\Column;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships;
|
use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships;
|
||||||
|
|
||||||
@@ -32,7 +33,22 @@ class Revision extends BaseModel
|
|||||||
/** Included rows */
|
/** Included rows */
|
||||||
public function rows()
|
public function rows()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Row::class, 'revision_row_pivot');
|
return $this->belongsToMany(Row::class, 'revision_row_pivot')->as('_row_pivot');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Included rows
|
||||||
|
* @param Column[] $columns
|
||||||
|
* @return \Illuminate\Database\Query\Builder|static
|
||||||
|
*/
|
||||||
|
public function rowsData($columns, $withId=true)
|
||||||
|
{
|
||||||
|
$selects = $withId ? ["data->>'_id' as _id"] : [];
|
||||||
|
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
$selects[] = "data->>'$col->id' as $col->name";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->rows()->select([])->selectRaw(implode(', ', $selects));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Proposal that lead to this revision */
|
/** Proposal that lead to this revision */
|
||||||
|
|||||||
+37
-10
@@ -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, always containing _grid
|
* @property string $data - JSONB, always containing _id
|
||||||
*/
|
*/
|
||||||
class Row extends BaseModel
|
class Row extends BaseModel
|
||||||
{
|
{
|
||||||
@@ -17,12 +17,12 @@ class Row extends BaseModel
|
|||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
public function getGridAttribute() {
|
public function getRowIdAttribute() {
|
||||||
return $this->data->_grid;
|
return $this->data->_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setGridAttribute($value) {
|
public function setRowIdAttribute($value) {
|
||||||
$this->data->_grid = $value;
|
$this->data->_id = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,21 +34,48 @@ class Row extends BaseModel
|
|||||||
*
|
*
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public static function allocateGRID()
|
public static function allocateRowID()
|
||||||
{
|
{
|
||||||
return \DB::selectOne("SELECT nextval('global_row_id_seq') AS grid;")->grid;
|
return \DB::selectOne("SELECT nextval('global_row_id_seq') AS rid;")->rid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocate a block of Global Row IDs, application-unique.
|
* Allocate a block of Global Row IDs, application-unique.
|
||||||
*
|
*
|
||||||
* @see Row::allocateGRID()
|
* @see Row::allocateRowID()
|
||||||
*
|
*
|
||||||
* @return int[] first and last
|
* @return int[] first and last
|
||||||
*/
|
*/
|
||||||
public static function allocateGRIDs($count)
|
public static function allocateRowIDs($count)
|
||||||
{
|
{
|
||||||
$last = \DB::selectOne("SELECT multi_nextval('global_row_id_seq', ?) AS last_grid;", [$count])->last_grid;
|
$last = \DB::selectOne("SELECT multi_nextval('global_row_id_seq', ?) AS last_id;", [$count])->last_id;
|
||||||
|
return [$last - $count + 1, $last];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate a single Global Column ID, application-unique.
|
||||||
|
*
|
||||||
|
* GCIDs are used to uniquely identify existing or proposed new columns,
|
||||||
|
* and are preserved after column modifications, to ensure change proposals have
|
||||||
|
* a clear target. This is the column equivalent of GRID.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function allocateColID()
|
||||||
|
{
|
||||||
|
return \DB::selectOne("SELECT nextval('global_column_id_seq') AS cid;")->cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate a block of Global Column IDs, application-unique.
|
||||||
|
*
|
||||||
|
* @see Row::allocateColID()
|
||||||
|
*
|
||||||
|
* @return int[] first and last
|
||||||
|
*/
|
||||||
|
public static function allocateColIDs($count)
|
||||||
|
{
|
||||||
|
$last = \DB::selectOne("SELECT multi_nextval('global_column_id_seq', ?) AS last_id;", [$count])->last_id;
|
||||||
return [$last - $count + 1, $last];
|
return [$last - $count + 1, $last];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,4 +226,14 @@ class User extends BaseModel implements
|
|||||||
|
|
||||||
$this->notify(new ConfirmEmail($newEmail, $confirmation->token));
|
$this->notify(new ConfirmEmail($newEmail, $confirmation->token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this user has a table with the givenname
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
*/
|
||||||
|
public function hasTable(string $name)
|
||||||
|
{
|
||||||
|
$this->tables()->where('name', $name)->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,14 +128,13 @@ abstract class BaseExporter
|
|||||||
|
|
||||||
$start = 0;
|
$start = 0;
|
||||||
while ($start < $count) {
|
while ($start < $count) {
|
||||||
$rows = $revision->rows()->offset($start)->limit($chunkSize)->get();
|
// TODO raw query to allow selecting aggregates, column subsets, etc
|
||||||
|
$rows = $revision->rowsData($this->columns, false)
|
||||||
|
->offset($start)->limit($chunkSize)->get()->toArray();
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$data = $row->data;
|
unset($row['_row_pivot']);
|
||||||
|
yield $row;
|
||||||
// column renaming, value formatting...
|
|
||||||
|
|
||||||
yield $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$start += $chunkSize;
|
$start += $chunkSize;
|
||||||
|
|||||||
@@ -99,20 +99,7 @@ class CStructArrayExporter extends BaseExporter
|
|||||||
echo ",";
|
echo ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
$val = 0;
|
$val = $row[$field->name];
|
||||||
switch ($field->type) {
|
|
||||||
case 'string':
|
|
||||||
$val = "";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'bool':
|
|
||||||
$val = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($row->{$field->name})) {
|
|
||||||
$val = $row->{$field->name};
|
|
||||||
}
|
|
||||||
|
|
||||||
// export to C format
|
// export to C format
|
||||||
switch ($field->type) {
|
switch ($field->type) {
|
||||||
|
|||||||
+32
-15
@@ -11,9 +11,10 @@ use MightyPork\Utils\Utils;
|
|||||||
/**
|
/**
|
||||||
* Helper class representing one column in a data table.
|
* Helper class representing one column in a data table.
|
||||||
*
|
*
|
||||||
|
* @property-read string $id
|
||||||
|
* @property-read string $type
|
||||||
* @property-read string $name
|
* @property-read string $name
|
||||||
* @property-read string $title
|
* @property-read string $title
|
||||||
* @property-read string $type
|
|
||||||
*/
|
*/
|
||||||
class Column implements JsonSerializable
|
class Column implements JsonSerializable
|
||||||
{
|
{
|
||||||
@@ -21,9 +22,10 @@ class Column implements JsonSerializable
|
|||||||
'int', 'bool', 'float', 'string'
|
'int', 'bool', 'float', 'string'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $id;
|
||||||
|
private $type;
|
||||||
private $name;
|
private $name;
|
||||||
private $title;
|
private $title;
|
||||||
private $type;
|
|
||||||
|
|
||||||
public static function columnsFromJson($columns)
|
public static function columnsFromJson($columns)
|
||||||
{
|
{
|
||||||
@@ -36,13 +38,13 @@ class Column implements JsonSerializable
|
|||||||
}, $columns);
|
}, $columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __get($name)
|
/**
|
||||||
|
* Set column ID
|
||||||
|
* @param string $id - GCID
|
||||||
|
*/
|
||||||
|
public function setID($id)
|
||||||
{
|
{
|
||||||
if (property_exists($this, $name)) {
|
$this->id = $id;
|
||||||
return $this->$name;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotApplicableException("No such column property");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,18 +54,32 @@ class Column implements JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public function __construct($obj)
|
public function __construct($obj)
|
||||||
{
|
{
|
||||||
$b = new \objBag($obj);
|
$b = objBag($obj);
|
||||||
|
|
||||||
|
if (!$b->has('name')) throw new NotApplicableException('Missing name in column');
|
||||||
|
if (!$b->has('type')) throw new NotApplicableException('Missing type in column');
|
||||||
|
|
||||||
|
if ($b->name[0] == '_') { // global row ID
|
||||||
|
throw new NotApplicableException("Column name can't start with underscore.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($b->type, self::colTypes)) {
|
||||||
|
throw new NotApplicableException("\"$b->type\" is not a valid column type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->id = $b->get('id', null);
|
||||||
$this->name = $b->name;
|
$this->name = $b->name;
|
||||||
$this->title = $b->title;
|
|
||||||
$this->type = $b->type;
|
$this->type = $b->type;
|
||||||
|
$this->title = $b->title ?: $b->name;
|
||||||
if ($this->name == '_grid') { // global row ID
|
|
||||||
throw new NotApplicableException("_grid is a reserved column name.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($this->type, self::colTypes)) {
|
public function __get($name)
|
||||||
throw new NotApplicableException("\"$this->type\" is not a valid column type.");
|
{
|
||||||
|
if (property_exists($this, $name)) {
|
||||||
|
return $this->$name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new NotApplicableException("No such column property: $name");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +88,7 @@ class Column implements JsonSerializable
|
|||||||
public function toArray()
|
public function toArray()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'type' => $this->type,
|
'type' => $this->type,
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Tables;
|
||||||
|
|
||||||
|
use App\Models\Row;
|
||||||
|
use MightyPork\Utils\Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a sequence of string codes for internal naming of columns
|
||||||
|
*
|
||||||
|
* Produces database-unique lowercase identifiers for created or proposed columns.
|
||||||
|
* Column identifiers are used in row objects to uniquely identify data even if the
|
||||||
|
* revision header (columns object) is modified, such as reordering, changing name,
|
||||||
|
* adding new column in concurrent proposals, etc.
|
||||||
|
*
|
||||||
|
* Thanks to this uniqueness, it could even be possible to compare or merge forks
|
||||||
|
* of the same table.
|
||||||
|
*/
|
||||||
|
class ColumnNumerator
|
||||||
|
{
|
||||||
|
const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
||||||
|
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
|
||||||
|
|
||||||
|
/** @var int */
|
||||||
|
private $next;
|
||||||
|
/** @var int */
|
||||||
|
private $last;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create numerator for the given number of columns
|
||||||
|
*/
|
||||||
|
public function __construct($capacity)
|
||||||
|
{
|
||||||
|
list($this->next, $this->last) = Row::allocateColIDs($capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next column name, incrementing the internal state
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function next()
|
||||||
|
{
|
||||||
|
if ($this->next > $this->last)
|
||||||
|
throw new \OutOfBoundsException("Column numerator has run out of allocated GCID slots");
|
||||||
|
|
||||||
|
$key = Utils::alphabetEncode($this->next, self::ALPHABET);
|
||||||
|
$this->next++;
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,15 +44,7 @@ class CsvExporter extends BaseExporter
|
|||||||
fputcsv($handle, $columnNames, $this->delimiter);
|
fputcsv($handle, $columnNames, $this->delimiter);
|
||||||
|
|
||||||
foreach ($this->iterateRows() as $row) {
|
foreach ($this->iterateRows() as $row) {
|
||||||
$items = [];
|
fputcsv($handle, array_values($row), $this->delimiter);
|
||||||
foreach ($this->columns as $column) {
|
|
||||||
if (isset($row->{$column->name})) {
|
|
||||||
$items[] = $row->{$column->name};
|
|
||||||
} else {
|
|
||||||
$items[] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fputcsv($handle, $items, $this->delimiter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class PhpExporter extends BaseExporter
|
|||||||
|
|
||||||
var_export($column->name);
|
var_export($column->name);
|
||||||
echo ' => ';
|
echo ' => ';
|
||||||
var_export($row->{$column->name});
|
var_export($row[$column->name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo ']';
|
echo ']';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Tables;
|
||||||
|
|
||||||
|
use App\Models\Row;
|
||||||
|
|
||||||
|
class RowNumerator
|
||||||
|
{
|
||||||
|
/** @var int */
|
||||||
|
private $next;
|
||||||
|
/** @var int */
|
||||||
|
private $last;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a numerator for the given number of rows.
|
||||||
|
*
|
||||||
|
* @param int $capacity
|
||||||
|
*/
|
||||||
|
public function __construct($capacity)
|
||||||
|
{
|
||||||
|
list($this->next, $this->last) = Row::allocateRowIDs($capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next()
|
||||||
|
{
|
||||||
|
if ($this->next > $this->last)
|
||||||
|
throw new \OutOfBoundsException("Row numerator has run out of allocated GRID slots");
|
||||||
|
|
||||||
|
return $this->next++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ END;
|
|||||||
$$ LANGUAGE 'plpgsql';
|
$$ LANGUAGE 'plpgsql';
|
||||||
PGSQL
|
PGSQL
|
||||||
);
|
);
|
||||||
DB::unprepared('CREATE SEQUENCE global_row_id_seq START 0 MINVALUE 0;');
|
DB::unprepared('CREATE SEQUENCE IF NOT EXISTS global_row_id_seq START 0 MINVALUE 0;');
|
||||||
// We have to increment manually once before the above function can be used - that is because
|
// 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
|
// 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
|
// the given step at all. This would give us negative values - not a problem in postgres without unsigned
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MakeGridColumnMandatoryInRowsTable extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
DB::unprepared("ALTER TABLE rows ADD CONSTRAINT grid_must_exist CHECK (data ? '_grid');");
|
DB::unprepared("ALTER TABLE rows ADD CONSTRAINT grid_must_exist CHECK (data ? '_id');");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddColumnSequence extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
DB::unprepared('CREATE SEQUENCE IF NOT EXISTS global_column_id_seq START 0 MINVALUE 0;');
|
||||||
|
// mandatory first increment to make the multi_nextval function work
|
||||||
|
DB::select("SELECT nextval('global_column_id_seq') as nv;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
DB::unprepared('DROP SEQUENCE IF EXISTS global_column_id_seq;');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -991,4 +991,26 @@ class Utils
|
|||||||
|
|
||||||
throw new FormatException("Bad time interval: \"$time\"");
|
throw new FormatException("Bad time interval: \"$time\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert number to arbitrary alphabet, using a system similar to EXCEL column numbering.
|
||||||
|
*
|
||||||
|
* @param int $n
|
||||||
|
* @param string[] $alphabet - array of characters to use
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function alphabetEncode($n, $alphabet)
|
||||||
|
{
|
||||||
|
$key = '';
|
||||||
|
$abcsize = count($alphabet);
|
||||||
|
$i = $n;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$mod = $i % $abcsize;
|
||||||
|
$key = $alphabet[$mod] . $key;
|
||||||
|
$i = (($i - $mod) / $abcsize) - 1;
|
||||||
|
} while($i >= 0);
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ class objBag implements JsonSerializable, ArrayAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $obj
|
||||||
|
* @return objBag
|
||||||
|
*/
|
||||||
function objBag($obj) {
|
function objBag($obj) {
|
||||||
return new \objBag($obj);
|
return new \objBag($obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,18 @@
|
|||||||
<table class="table table-hover table-sm">
|
<table class="table table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#GRID</th>
|
<th>_id</th>
|
||||||
@foreach($columns as $col)
|
@foreach($columns as $col)
|
||||||
<th>{{ $col->title }}</th>
|
<th>{{$col->name}} ("{{ $col->title }}") [ {{$col->id}} ]</th>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($rows as $row)
|
@foreach($rows as $row)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $row->data->_grid }}</td>
|
<td>{{ $row->_id }}</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->{$col->name} }}</td>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
+7
-2
@@ -16,7 +16,9 @@ Route::get('/auth/resend-email-confirmation', 'Auth\ConfirmEmailController@resen
|
|||||||
|
|
||||||
// ----------------- SOCIAL LOGIN --------------------
|
// ----------------- SOCIAL LOGIN --------------------
|
||||||
|
|
||||||
function _loginVia($method) {
|
if (!function_exists('_loginVia')) {
|
||||||
|
function _loginVia($method)
|
||||||
|
{
|
||||||
$wasLoggedIn = !guest();
|
$wasLoggedIn = !guest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -49,6 +51,8 @@ function _loginVia($method) {
|
|||||||
$cnt++;
|
$cnt++;
|
||||||
$user->name = $basename . $cnt;
|
$user->name = $basename . $cnt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user->title = $basename;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set e-mail from provider data, only if user e-mail is empty
|
// set e-mail from provider data, only if user e-mail is empty
|
||||||
@@ -67,7 +71,8 @@ function _loginVia($method) {
|
|||||||
return redirect(route('profile.manage-oauth'));
|
return redirect(route('profile.manage-oauth'));
|
||||||
else
|
else
|
||||||
return Redirect::intended();
|
return Redirect::intended();
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Route::get('/auth/github/authorize', function() {
|
Route::get('/auth/github/authorize', function() {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class ExampleTest extends TestCase
|
|||||||
public function testBasicTest()
|
public function testBasicTest()
|
||||||
{
|
{
|
||||||
$response = $this->get('/');
|
$response = $this->get('/');
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Tables\ColumnNumerator;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ColumnNumeratorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIncrementsWithDefaultInit()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator();
|
||||||
|
|
||||||
|
self::assertEquals('a', $n->next());
|
||||||
|
self::assertEquals('b', $n->next());
|
||||||
|
self::assertEquals('c', $n->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIncrementsWithCustomInit()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator('r');
|
||||||
|
|
||||||
|
self::assertEquals('r', $n->next());
|
||||||
|
self::assertEquals('s', $n->next());
|
||||||
|
self::assertEquals('t', $n->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIncrementsMultiChar()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator('aaa');
|
||||||
|
|
||||||
|
self::assertEquals('aaa', $n->next());
|
||||||
|
self::assertEquals('aab', $n->next());
|
||||||
|
self::assertEquals('aac', $n->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOverflowGrow()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator('y');
|
||||||
|
|
||||||
|
self::assertEquals('y', $n->next());
|
||||||
|
self::assertEquals('z', $n->next());
|
||||||
|
self::assertEquals('aa', $n->next());
|
||||||
|
self::assertEquals('ab', $n->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOverflowNoGrow()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator('yy');
|
||||||
|
|
||||||
|
self::assertEquals('yy', $n->next());
|
||||||
|
self::assertEquals('yz', $n->next());
|
||||||
|
self::assertEquals('za', $n->next());
|
||||||
|
self::assertEquals('zb', $n->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResume()
|
||||||
|
{
|
||||||
|
$n = new ColumnNumerator('yy');
|
||||||
|
self::assertEquals('yy', $n->next());
|
||||||
|
self::assertEquals('yz', $n->next());
|
||||||
|
|
||||||
|
$n2 = new ColumnNumerator($n->getState());
|
||||||
|
unset($n);
|
||||||
|
|
||||||
|
self::assertEquals('za', $n2->next(), 'recreated numerator continues');
|
||||||
|
self::assertEquals('zb', $n2->next());
|
||||||
|
|
||||||
|
self::assertEquals($n2->getState(), 'zc', 'State is next string');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use Tests\TestCase;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testBasicTest()
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user