add column unique numbering scheme, more efficient selects
This commit is contained in:
@@ -3,15 +3,16 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Revision;
|
||||
use App\Models\Row;
|
||||
use App\Models\Table;
|
||||
use App\Models\User;
|
||||
use App\Tables\Column;
|
||||
use App\Tables\ColumnNumerator;
|
||||
use App\Tables\CStructArrayExporter;
|
||||
use App\Tables\CsvExporter;
|
||||
use App\Tables\CXMacroExporter;
|
||||
use App\Tables\JsonExporter;
|
||||
use App\Tables\PhpExporter;
|
||||
use App\Tables\RowNumerator;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use MightyPork\Exceptions\NotApplicableException;
|
||||
@@ -41,11 +42,15 @@ class TableController extends Controller
|
||||
|
||||
$this->countTableVisit($request, $tableModel);
|
||||
|
||||
$columns = Column::columnsFromJson($revision->columns);
|
||||
|
||||
$rows = $revision->rowsData($columns)->paginate(25, []);
|
||||
|
||||
return view('table.view', [
|
||||
'table' => $tableModel,
|
||||
'revision' => $revision,
|
||||
'columns' => Column::columnsFromJson($revision->columns),
|
||||
'rows' => $revision->rows()->paginate(25),
|
||||
'columns' => $columns,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -78,6 +83,7 @@ class TableController extends Controller
|
||||
"Patella vulgata,common limpet,20";
|
||||
|
||||
$columns = Column::columnsFromJson([
|
||||
// fake 'id' to satisfy the check in Column constructor
|
||||
['name' => 'latin', 'type' => 'string', 'title' => 'Latin Name'],
|
||||
['name' => 'common', 'type' => 'string', 'title' => 'Common Name'],
|
||||
['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)
|
||||
{
|
||||
/** @var Table $tableModel */
|
||||
@@ -102,6 +111,9 @@ class TableController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store modified table settings
|
||||
*/
|
||||
public function storeSettings(Request $request, User $user, string $table)
|
||||
{
|
||||
/** @var Table $tableModel */
|
||||
@@ -149,85 +161,100 @@ class TableController extends Controller
|
||||
]);
|
||||
|
||||
// Check if table name is unique for user
|
||||
if ($u->tables()->where('name', $input->name)->exists()) {
|
||||
if ($u->hasTable($input->name)) {
|
||||
return $this->backWithErrors([
|
||||
'name' => "A table called \"$input->name\" already exists in your account.",
|
||||
]);
|
||||
}
|
||||
|
||||
// --- COLUMNS ---
|
||||
// Parse and validate the columns specification
|
||||
/** @var Column[] $columns */
|
||||
$columns = [];
|
||||
$column_keys = []; // for checking duplicates
|
||||
$col_names = []; // for checking duplicates
|
||||
$colsArray = fromJSON($input->columns);
|
||||
|
||||
// prevent griefing via long list of columns
|
||||
if (count($colsArray) > 100) return $this->backWithErrors(['columns' => "Too many columns"]);
|
||||
|
||||
foreach ($colsArray as $col) {
|
||||
if (!isset($col->name) || !isset($col->type) || empty($col->name) || empty($col->type)) {
|
||||
return $this->backWithErrors(['columns' => "All columns must have at least name and type."]);
|
||||
foreach ($colsArray as $colObj) {
|
||||
// ensure column has a title
|
||||
if (!isset($colObj->title)) {
|
||||
$colObj->title = $colObj->name;
|
||||
}
|
||||
|
||||
try {
|
||||
if (in_array($col->name, $column_keys)) {
|
||||
return $this->backWithErrors(['columns' => "Duplicate column: $col->name"]);
|
||||
if (in_array($colObj->name, $col_names)) {
|
||||
return $this->backWithErrors(['columns' => "Duplicate column: $colObj->name"]);
|
||||
}
|
||||
$column_keys[] = $col->name;
|
||||
|
||||
if (!isset($col->title)) $col->title = $col->name;
|
||||
$columns[] = new Column($col);
|
||||
$col_names[] = $colObj->name;
|
||||
$columns[] = new Column($colObj);
|
||||
} catch (\Exception $e) {
|
||||
// validation errors from the Column constructor
|
||||
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
|
||||
$rowsData = null;
|
||||
$rowsToInsert = null;
|
||||
$rowNumerator = null;
|
||||
try {
|
||||
$grid_range = Row::allocateGRIDs(count($rowTable));
|
||||
$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;
|
||||
$rowsToInsert = collect($dataCsvLines)->map(function ($row) use ($columns) {
|
||||
if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null; // discard empty lines
|
||||
if (count($row) != count($columns)) {
|
||||
throw new NotApplicableException("All rows must have " . count($columns) . " fields.");
|
||||
}
|
||||
|
||||
$parsed = [
|
||||
'_grid' => $grid_cnt++
|
||||
];
|
||||
$data = [];
|
||||
|
||||
foreach ($row as $i => $val) {
|
||||
$key = $columns[$i]->name;
|
||||
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);
|
||||
$col = $columns[$i];
|
||||
|
||||
$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) {
|
||||
return $this->backWithErrors(['data' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
// --- STORE TO DB ---
|
||||
|
||||
/* Fields for the new Revision instance */
|
||||
$revisionFields = [
|
||||
'note' => "Initial revision of table $u->name/$input->name",
|
||||
'columns' => $columns,
|
||||
'row_count' => count($rowsData),
|
||||
'row_count' => count($rowsToInsert),
|
||||
];
|
||||
|
||||
/* Fields for the new Table instance */
|
||||
$tableFields = [
|
||||
'owner_id' => $u->id,
|
||||
'revision_id' => 0,
|
||||
'name' => $input->name,
|
||||
'title' => $input->title,
|
||||
'description' => $input->description,
|
||||
@@ -235,10 +262,12 @@ class TableController extends Controller
|
||||
'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);
|
||||
|
||||
$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);
|
||||
|
||||
// Attach the revision to the table, set as current
|
||||
@@ -246,10 +275,10 @@ class TableController extends Controller
|
||||
$table->revision()->associate($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)
|
||||
{
|
||||
$cookieName = "view_$table->id";
|
||||
$cookieName = "view_{$table->owner->name}_{$table->name}";
|
||||
if (!$request->cookie($cookieName, false)) {
|
||||
$ua = $request->userAgent();
|
||||
// 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)
|
||||
{
|
||||
/** @var Table $tableModel */
|
||||
|
||||
Reference in New Issue
Block a user