logo, improved listing, some helpers, mail confirms table, confirmed flag, user title column..

This commit is contained in:
2018-07-22 15:43:17 +02:00
parent 9081b38425
commit a3ba68ea98
47 changed files with 1462 additions and 292 deletions
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class ConfirmUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:confirm {user}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Confirm user\'s e-mail (for testing)';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$u = User::resolve($this->argument('user'));
$u->update('confirmed', true);
$this->info("User #$u->id with e-mail $u->email and handle @$u->name was confirmed.");
}
}
@@ -25,7 +25,7 @@ class LoginController extends Controller
*
* @var string
*/
protected $redirectTo = '/home';
protected $redirectTo = '/';
/**
* Create a new controller instance.
@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Validation\Rule;
class RegisterController extends Controller
{
@@ -28,7 +29,7 @@ class RegisterController extends Controller
*
* @var string
*/
protected $redirectTo = '/home';
protected $redirectTo = '/';
/**
* Create a new controller instance.
@@ -49,9 +50,15 @@ class RegisterController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255|regex:/^[a-zA-Z0-9_.-]+$/',
'name' => [
'regex:/^[a-zA-Z0-9_.-]+$/',
'required',
'string',
'max:255',
'unique:users'
],
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
'password' => 'required|string|min:6|max:1000|confirmed', // max len to foil DOS attempts
]);
}
@@ -65,7 +72,7 @@ class RegisterController extends Controller
{
return User::create([
'name' => $data['name'],
'title' => $data['name'], // display name - by default, init to name
'title' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
-20
View File
@@ -1,20 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HomeController extends Controller
{
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$tables = \Auth::user()->tables()->paginate(10);
return view('home')->with(compact('tables'));
}
}
+39 -25
View File
@@ -8,6 +8,7 @@ use App\Models\Table;
use App\Models\User;
use App\Utils\Column;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use MightyPork\Exceptions\NotApplicableException;
class TableController extends Controller
@@ -16,7 +17,7 @@ class TableController extends Controller
{
/** @var Table $tableModel */
$tableModel = $user->tables()->where('name', $table)->first();
$revision = $tableModel->activeRevision;
$revision = $tableModel->revision;
return view('table.view', [
'table' => $tableModel,
@@ -54,12 +55,12 @@ class TableController extends Controller
$u = \Auth::user();
$this->validate($request, [
'name' => 'required',
'title' => 'string',
'description' => 'string|nullable',
'license' => 'string|nullable',
'origin' => 'string|nullable',
'columns' => 'required',
'name' => 'required|string|max:255',
'title' => 'string|string|max:255',
'description' => 'string|nullable|max:4000',
'license' => 'string|nullable|max:4000',
'origin' => 'string|nullable|max:4000',
'columns' => 'required|string',
'data' => 'string|nullable',
]);
@@ -74,16 +75,24 @@ class TableController extends Controller
// Parse and validate the columns specification
/** @var Column[] $columns */
$columns = [];
$column_keys = []; // for checking duplicates
$colTable = array_map('str_getcsv', explode("\n", $request->get('columns')));
// prevent griefing via long list of columns
if (count($colTable) > 100) return $this->backWithErrors(['columns' => "Too many 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.",
]);
if (count($col) < 2 || strlen($col[0])==0) {
return $this->backWithErrors(['columns' => "All columns must have at least name and type."]);
}
try {
if (in_array($col[0], $column_keys)) {
return $this->backWithErrors(['columns' => "Duplicate column: $col[0]"]);
}
$column_keys[] = $col[0];
$columns[] = new Column([
'name' => $col[0],
'type' => $col[1],
@@ -93,6 +102,7 @@ class TableController extends Controller
return $this->backWithErrors(['columns' => $e->getMessage()]);
}
}
if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]);
$rowTable = array_map('str_getcsv', explode("\n", $request->get('data')));
@@ -110,38 +120,42 @@ class TableController extends Controller
}
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
$revisionFields = [
'note' => "Initial revision of table $u->name/$tabName",
'columns' => json_encode($columns),
]);
'row_count' => count($rowsData),
];
$table = Table::create([
$tableFields = [
'owner_id' => $u->id,
'revision_id' => $revision->id,
'revision_id' => 0,
'name' => $tabName,
'title' => $request->get('title'),
'description' => $request->get('description'),
'license' => $request->get('license'),
'origin' => $request->get('origin'),
]);
];
// Attach the revision to the table, set as current
$table->revisions()->attach($revision);
$table->activeRevision()->associate($revision);
\DB::transaction(function () use ($revisionFields, $tableFields, $rowsData) {
$revision = Revision::create($revisionFields);
// Spawn the rows, linked to the revision
$revision->rows()->createMany($rowsData);
$tableFields['revision_id'] = $revision->id; // to meet the not-null constraint
$table = Table::create($tableFields);
// Now we create rows, a revision pointing to them, and the table using it.
// Attach the revision to the table, set as current
$table->revisions()->attach($revision);
$table->revision()->associate($revision);
return "Ok.";
// Spawn rows, linked to the revision
$revision->rows()->createMany($rowsData);
});
return redirect(route('table.view', ['user' => $u, 'table' => $tabName]));
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller
{
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function view(User $user)
{
$tables = $user->tables()
->with('revision:id,row_count')
->paginate(3);
return view('user.view')->with(compact('tables', 'user'));
}
}
+1 -1
View File
@@ -14,8 +14,8 @@ use Illuminate\Database\Eloquent\Model;
* @property int $object_id
* @property int $author_id
* @property string $message
* @property User $author
* @property mixed $object - morph
* @property-read User $author
*/
class ContentReport extends Model
{
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* E-mail confirmation
*
* @property int $id
* @property \Carbon\Carbon $created_at
* @property int $user_id
* @property string $token
* @property-read User $user
*/
class EmailConfirmation extends Model
{
protected $guarded = [];
const UPDATED_AT = null; // disable update timestamp
/** Authoring user */
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
+3 -3
View File
@@ -15,10 +15,10 @@ use Illuminate\Database\Eloquent\Model;
* @property int $revision_id
* @property int $author_id
* @property string $note
* @property User $author
* @property Table $table
* @property Revision $revision
* @property object $changes - JSONB
* @property-read User $author
* @property-read Table $table
* @property-read Revision $revision
*/
class Proposal extends Model
{
+5 -4
View File
@@ -15,10 +15,11 @@ use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships;
* @property int $ancestor_id
* @property string $note
* @property object $columns
* @property Revision|null $parentRevision
* @property Row[]|Collection $rows
* @property Proposal|null $sourceProposal - proposal that was used to create this revision
* @property Proposal[]|Collection $dependentProposals
* @property int $row_count - cached number of rows in the revision
* @property-read Revision|null $parentRevision
* @property-read Row[]|Collection $rows
* @property-read Proposal|null $sourceProposal - proposal that was used to create this revision
* @property-read Proposal[]|Collection $dependentProposals
*/
class Revision extends Model
{
+12 -10
View File
@@ -2,9 +2,11 @@
namespace App\Models;
use App\Models\Concerns\ManyManyThrough;
use App\Models\Concerns\Reportable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships;
/**
* A data table object (referencing revisions)
@@ -20,15 +22,15 @@ use Illuminate\Database\Eloquent\Model;
* @property string $description
* @property string $license
* @property string $origin
* @property User $owner
* @property Table $parentTable
* @property Table[]|Collection $forks
* @property Revision[]|Collection $revisions
* @property Revision $activeRevision
* @property Proposal[]|Collection $proposal
* @property TableComment[]|Collection $comments
* @property User[]|Collection $favouritingUsers
* @property User[]|Collection $discussionFollowers
* @property-read User $owner
* @property-read Table $parentTable
* @property-read Table[]|Collection $forks
* @property-read Revision[]|Collection $revisions
* @property-read Revision $revision
* @property-read Proposal[]|Collection $proposal
* @property-read TableComment[]|Collection $comments
* @property-read User[]|Collection $favouritingUsers
* @property-read User[]|Collection $discussionFollowers
*/
class Table extends Model
{
@@ -60,7 +62,7 @@ class Table extends Model
}
/** Active revision */
public function activeRevision()
public function revision()
{
return $this->belongsTo(Revision::class, 'revision_id');
}
+4 -4
View File
@@ -16,10 +16,10 @@ use Illuminate\Database\Eloquent\Model;
* @property int $author_id
* @property int $ancestor_id
* @property string $message
* @property User $author
* @property Table $table
* @property TableComment|null $ancestor
* @property TableComment[]|Collection $replies
* @property-read User $author
* @property-read Table $table
* @property-read TableComment|null $ancestor
* @property-read TableComment[]|Collection $replies
*/
class TableComment extends Model
{
+44 -16
View File
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notification;
use MightyPork\Exceptions\ArgumentException;
use MightyPork\Exceptions\NotExistException;
/**
* A user in the application
@@ -19,16 +21,18 @@ use Illuminate\Notifications\Notification;
* @property string $title - for display
* @property string $email - unique, for login and social auth chaining
* @property string $password - hashed pw
* @property Proposal[]|Collection $proposals
* @property Table[]|Collection $tables
* @property OAuthIdentity[]|Collection $socialIdentities
* @property TableComment[]|Collection $tableComments
* @property Table[]|Collection $favouriteTables
* @property Table[]|Collection $followedDiscussions
* @property ContentReport[]|Collection $authoredReports
* @property Notification[]|Collection $notifications
* @property Notification[]|Collection $readNotifications
* @property Notification[]|Collection $unreadNotifications
* @property bool $confirmed - user e-mail is confirmed
* @property-read Proposal[]|Collection $proposals
* @property-read Table[]|Collection $tables
* @property-read OAuthIdentity[]|Collection $socialIdentities
* @property-read TableComment[]|Collection $tableComments
* @property-read Table[]|Collection $favouriteTables
* @property-read Table[]|Collection $followedDiscussions
* @property-read ContentReport[]|Collection $authoredReports
* @property-read Notification[]|Collection $notifications
* @property-read Notification[]|Collection $readNotifications
* @property-read Notification[]|Collection $unreadNotifications
* @property-read string $handle - user handle, including @
*/
class User extends Authenticatable
{
@@ -137,12 +141,36 @@ class User extends Authenticatable
$this->followedDiscussions()->detach($table);
}
public function fakeTables()
/**
* Resolve user by a given name, id, or e-mail
*
* @param string $ident
* @return User
*/
public static function resolve($ident)
{
return [
(object)['title' => 'Table 1'],
(object)['title' => 'Table 2'],
(object)['title' => 'Table 3']
];
$ident = "$ident";
if (strlen($ident) == 0) throw new ArgumentException("Can't find user by empty string.");
if (is_numeric($ident)) {
$u = static::find((int)$ident);
}
else if ($ident[0] == '@') {
$u = static::where('name', substr($ident, 1))->first();
}
else {
$u = static::where('email', $ident)->first();
}
if (!$u) throw new NotExistException("No user found matching \"$ident\"");
return $u;
}
public function __get($name)
{
if ($name == 'handle') return "@$this->name";
return parent::__get($name);
}
}
+2 -1
View File
@@ -22,7 +22,8 @@ class AppServiceProvider extends ServiceProvider
\Route::bind('user', function ($value) {
$u = User::where('name', $value)->first();
// it may also be the _id directly
if (!$u) $u = User::find($value);
if (!$u && is_numeric($value)) $u = User::find((int)$value);
if (!$u) abort(404);
return $u;
});
}
+1 -5
View File
@@ -53,11 +53,7 @@ class RouteServiceProvider extends ServiceProvider
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/public.php'));
Route::middleware(['web', 'auth'])
->namespace($this->namespace)
->group(base_path('routes/user.php'));
->group(base_path('routes/web.php'));
}
/**
+4 -3
View File
@@ -19,9 +19,10 @@ class Widget
{
protected $attributesArray = [],
$id, $name, $label, $value,
$viewName,
$help,
$labelCols, $fieldCols,
$viewName, // view to use inside the form namespace
$help, // help button with tooltip
$prepend, $append, // input-group (add-ons)
$labelCols, $fieldCols, // grid layout setting (label/field cols)
$styleArray = [];
public function __construct($viewName, $name, $label)
+21
View File
@@ -1,3 +1,24 @@
<?php
// global helpers
function authed() {
return ! \Auth::guest();
}
function guest() {
return \Auth::guest();
}
function user() {
return \Auth::user();
}
function ellipsis($s, $len) {
return \MightyPork\Utils\Str::ellipsis($s, $len);
}
function faker() {
static $fac = null;
if ($fac !== null) return $fac;
return $fac = Faker\Factory::create();
}