add column unique numbering scheme, more efficient selects

pull/26/head
Ondřej Hruška 6 years ago
parent 69bda25bbb
commit fabc3ad24e
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      app/Console/Commands/ConfirmUser.php
  2. 116
      app/Http/Controllers/TableController.php
  3. 4
      app/Models/BaseModel.php
  4. 18
      app/Models/Revision.php
  5. 47
      app/Models/Row.php
  6. 10
      app/Models/User.php
  7. 11
      app/Tables/BaseExporter.php
  8. 15
      app/Tables/CStructArrayExporter.php
  9. 47
      app/Tables/Column.php
  10. 52
      app/Tables/ColumnNumerator.php
  11. 10
      app/Tables/CsvExporter.php
  12. 2
      app/Tables/PhpExporter.php
  13. 32
      app/Tables/RowNumerator.php
  14. 2
      database/migrations/2018_08_01_204822_add_row_sequence_generator.php
  15. 2
      database/migrations/2018_08_01_213353_make_grid_column_mandatory_in_rows_table.php
  16. 30
      database/migrations/2018_08_04_165815_add_column_sequence.php
  17. 22
      porklib/Utils/Utils.php
  18. 4
      porklib/helpers.php
  19. 8
      resources/views/table/_rows.blade.php
  20. 9
      routes/login.php
  21. 1
      tests/Feature/ExampleTest.php
  22. 72
      tests/Unit/ColumnNumeratorTest.php
  23. 19
      tests/Unit/ExampleTest.php

@ -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) {
if (strlen($val) > 1000) {
// try to stop people inserting unstructured crap / malformed CSV // try to stop people inserting unstructured crap / malformed CSV
throw new NotApplicableException("Value for column $key too long."); throw new NotApplicableException("Value for column {$col->name} too long.");
} }
$parsed[$key] = $columns[$i]->cast($val); $data[$col->id] = $col->cast($val);
} }
return [
'data' => $parsed,
];
}, $rowTable);
$rowsData = array_filter($rowsData); // 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));
} }

@ -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 */

@ -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) {

@ -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

@ -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);
}
}
Loading…
Cancel
Save