diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 8663980..4b67cba 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -2,8 +2,13 @@ namespace App\Http\Controllers; +use App\Models\Revision; +use App\Models\Row; use App\Models\Table; +use App\Models\User; +use App\Utils\Column; use Illuminate\Http\Request; +use MightyPork\Exceptions\NotApplicableException; class TableController extends Controller { @@ -15,14 +20,14 @@ class TableController extends Controller public function create() { $exampleColumns = - "latin, string, Latin Name\n". - "common, string, Common Name\n". - "lifespan, int, Lifespan (years)"; + "latin,string,Latin Name\n". + "common,string,Common Name\n". + "lifespan,int,Lifespan (years)"; $exampleData = - "Mercenaria mercenaria, hard clam, 40\n" . - "Magallana gigas, pacific oyster, 30\n" . - "Patella vulgata, common limpet, 20"; + "Mercenaria mercenaria,hard clam,40\n" . + "Magallana gigas,pacific oyster,30\n" . + "Patella vulgata,common limpet,20"; return view('table.create', compact('exampleColumns', 'exampleData') @@ -31,31 +36,91 @@ class TableController extends Controller public function storeNew(Request $request) { + /** @var User $u */ $u = \Auth::user(); $this->validate($request, [ 'name' => 'required', - 'title' => 'string|nullable', + 'title' => 'string', 'description' => 'string|nullable', 'license' => 'string|nullable', - 'upstream' => 'string|nullable', + 'origin' => 'string|nullable', 'columns' => 'required', 'data' => 'string|nullable', ]); // Check if table name is unique for user - $name = $request->get('name'); - if ($u->tables()->where('name', $name)->exists()) { + $tabName = $request->get('name'); + if ($u->tables()->where('name', $tabName)->exists()) { return $this->backWithErrors([ - 'name' => "A table called \"$name\" already exists in your account.", + 'name' => "A table called \"$tabName\" already exists in your account.", ]); } // Parse and validate the columns specification + /** @var Column[] $columns */ + $columns = []; + $colTable = array_map('str_getcsv', explode("\n", $request->get('columns'))); + foreach ($colTable as $col) { + $col = array_map('trim', $col); + if (count($col) < 2) { + return $this->backWithErrors([ + 'columns' => "All columns must have at least name and type.", + ]); + } - // Now we create rows, a revision pointing to them, and the table using it. + try { + $columns[] = new Column([ + 'name' => $col[0], + 'type' => $col[1], + 'title' => count($col) >= 3 ? $col[2] : $col[0], // title falls back to =name if not specified, + ]); + } catch (\Exception $e) { + return $this->backWithErrors(['columns' => $e->getMessage()]); + } + } + + $rowTable = array_map('str_getcsv', explode("\n", $request->get('data'))); + + $rowsData = null; + try { + $rowsData = array_map(function ($row) use ($columns) { + if (count($row) != count($columns)) { + throw new NotApplicableException("All rows must have ".count($columns)." fields."); + } + $parsed = []; + foreach ($row as $i => $val) { + $key = $columns[$i]->name; + $parsed[$key] = $columns[$i]->cast($val); + } + return [ + 'data' => json_encode($parsed), + 'refs' => 1, + ]; + }, $rowTable); + }catch (\Exception $e) { + return $this->backWithErrors(['columns' => $e->getMessage()]); + } + $revision = Revision::create([ + 'refs' => 1, // from the new table + 'note' => "Initial revision of table $u->name/$tabName", + 'columns' => json_encode($columns), + ]); + + $table = Table::create([ + 'owner_id' => $u->id, + 'revision_id' => $revision->id, + 'name' => $tabName, + 'title' => $request->get('title'), + 'description' => $request->get('description'), + 'license' => $request->get('license'), + 'origin' => $request->get('origin'), + ]); + $revision->rows()->createMany($rowsData); + + // Now we create rows, a revision pointing to them, and the table using it. return "Ok."; } diff --git a/app/Models/ContentReport.php b/app/Models/ContentReport.php index f3ad46c..d79e369 100644 --- a/app/Models/ContentReport.php +++ b/app/Models/ContentReport.php @@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Model; */ class ContentReport extends Model { + protected $guarded = []; + /** Authoring user */ public function author() { diff --git a/app/Models/Proposal.php b/app/Models/Proposal.php index 09ed1b1..f47ddb9 100644 --- a/app/Models/Proposal.php +++ b/app/Models/Proposal.php @@ -23,6 +23,7 @@ use Illuminate\Database\Eloquent\Model; class Proposal extends Model { use Reportable; + protected $guarded = []; protected static function boot() { diff --git a/app/Models/Revision.php b/app/Models/Revision.php index 979a6fb..0287a05 100644 --- a/app/Models/Revision.php +++ b/app/Models/Revision.php @@ -15,7 +15,7 @@ use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships; * @property int $refs * @property int $ancestor_id * @property string $note - * @property string $index_column + * @property object $columns * @property Revision|null $parentRevision * @property Row[]|Collection $rows * @property Proposal|null $sourceProposal - proposal that was used to create this revision @@ -24,6 +24,7 @@ use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships; class Revision extends Model { use HasRelaquentRelationships; + protected $guarded = []; protected static function boot() { diff --git a/app/Models/Row.php b/app/Models/Row.php index b217c72..f7c3214 100644 --- a/app/Models/Row.php +++ b/app/Models/Row.php @@ -13,5 +13,6 @@ use Illuminate\Database\Eloquent\Model; */ class Row extends Model { + protected $guarded = []; public $timestamps = false; } diff --git a/app/Models/Table.php b/app/Models/Table.php index ed8a0cc..d7821d3 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -15,10 +15,11 @@ use Illuminate\Database\Eloquent\Model; * @property int $owner_id * @property int $ancestor_id * @property int $revision_id + * @property string $name * @property string $title * @property string $description * @property string $license - * @property string $source_link + * @property string $origin * @property User $owner * @property Table $parentTable * @property Table[]|Collection $forks @@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\Model; class Table extends Model { use Reportable; + protected $guarded = []; protected static function boot() { diff --git a/app/Models/TableComment.php b/app/Models/TableComment.php index 26b23a0..6b65e30 100644 --- a/app/Models/TableComment.php +++ b/app/Models/TableComment.php @@ -24,6 +24,7 @@ use Illuminate\Database\Eloquent\Model; class TableComment extends Model { use Reportable; + protected $guarded = []; protected static function boot() { diff --git a/app/Utils/Column.php b/app/Utils/Column.php new file mode 100644 index 0000000..f3d66b2 --- /dev/null +++ b/app/Utils/Column.php @@ -0,0 +1,108 @@ +$name; + } + + throw new NotApplicableException("No such column property"); + } + + /** + * Create from object or array + * + * @param $obj + */ + public function __construct($obj) + { + $b = new \objBag($obj); + $this->name = $b->name; + $this->title = $b->title; + $this->type = $b->type; + + if (!in_array($this->type, self::colTypes)) { + throw new NotApplicableException("\"$this->type\" is not a valid column type."); + } + } + + /** + * @return array with keys {name, title, type} + */ + public function toArray() + { + return [ + 'name' => $this->name, + 'title' => $this->title, + 'type' => $this->type, + ]; + } + + /** + * Convert a value to the target type, validating it in the process + * + * @param mixed $value + * @return bool|float|int|string + */ + public function cast($value) + { + switch ($this->type) { + case 'int': + if (is_int($value)) return $value; + if (is_float($value)) return round($value); + if (is_numeric($value)) return intval($value); + throw new NotApplicableException("Could not convert value \"$value\" to int!"); + + case 'float': + if (is_int($value) || is_float($value)) return (float)$value; + if (is_numeric($value)) return floatval($value); + throw new NotApplicableException("Could not convert value \"$value\" to float!"); + + case 'bool': + return Utils::parseBool($value); + + case 'string': + return "$value"; + + default: + throw new \LogicException("Illegal column type: \"$this->type\""); + } + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize() + { + return $this->toArray(); + } +} diff --git a/database/migrations/2018_07_08_193600_create_revisions_table.php b/database/migrations/2018_07_08_193600_create_revisions_table.php index 2b32f8f..b5170fb 100644 --- a/database/migrations/2018_07_08_193600_create_revisions_table.php +++ b/database/migrations/2018_07_08_193600_create_revisions_table.php @@ -24,7 +24,7 @@ class CreateRevisionsTable extends Migration $table->jsonb('columns'); // author's note describing what has been changed - $table->text('note'); + $table->text('note')->nullable(); $table->foreign('ancestor_id')->references('id')->on('revisions') ->onDelete('set null'); diff --git a/database/migrations/2018_07_08_193700_create_tables_table.php b/database/migrations/2018_07_08_193700_create_tables_table.php index 9738fd0..0acc71c 100644 --- a/database/migrations/2018_07_08_193700_create_tables_table.php +++ b/database/migrations/2018_07_08_193700_create_tables_table.php @@ -21,9 +21,9 @@ class CreateTablesTable extends Migration $table->unsignedInteger('revision_id')->index(); // active revision $table->string('name')->index(); // indexable $table->string('title')->index(); // indexable - $table->text('description'); - $table->text('license'); - $table->text('source_link'); + $table->text('description')->nullable(); + $table->text('license')->nullable(); + $table->text('origin')->nullable(); $table->foreign('owner_id')->references('id')->on('users') ->onDelete('restrict'); // user deleting their account deletes owned tables diff --git a/database/migrations/2018_07_08_194000_create_proposals_table.php b/database/migrations/2018_07_08_194000_create_proposals_table.php index 8cf2f9f..7138357 100644 --- a/database/migrations/2018_07_08_194000_create_proposals_table.php +++ b/database/migrations/2018_07_08_194000_create_proposals_table.php @@ -23,7 +23,7 @@ class CreateProposalsTable extends Migration $table->unsignedInteger('revision_id')->index(); // parent revision (applying it to a different revisions may cause conflicts) $table->unsignedInteger('author_id')->index(); - $table->text('note'); + $table->text('note')->nullable(); $table->jsonb('changes'); // the actual meat of the changeset is stored here diff --git a/porklib/Exceptions/NotApplicableException.php b/porklib/Exceptions/NotApplicableException.php new file mode 100644 index 0000000..084756f --- /dev/null +++ b/porklib/Exceptions/NotApplicableException.php @@ -0,0 +1,11 @@ +help('License applicable to the table\'s data, if any. By default, all tables are CC0 or Public Domain.') !!} - {!! Widget::text('upstream', 'Adapted from') + {!! Widget::text('origin', 'Adapted from') ->help('If you took the data from some external site, a book, etc., write it here. URLs in a full format will be clickable.') !!}