|
|
|
@ -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) { |
|
|
|
|
$col = $columns[$i]; |
|
|
|
|
|
|
|
|
|
if (strlen($val) > 1000) { |
|
|
|
|
// 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) { |
|
|
|
|
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 */ |
|
|
|
|