Show data from File field on pivot table in Laravel Nova - laravel-5.8

Before a user submission can go live, and admin needs to verify the data that is provided. This data is stored in the pivot table specialism_user.
Both the specialism and the user Resource have a BelongsToMany::make field on them:
class Specialism extends Resource
{
public function fields(Request $request)
{
return [
BelongsToMany::make('Gebruikers', 'Users', User::class)
->fields(function() {
return [
Boolean::make('Goedgekeurd', 'approved'),
File::make('Diploma', 'diploma'),
File::make('Cijferlijst', 'cijferlijst'),
File::make('Overige', 'certificate'),
Date::make('Geldig', 'valid_until'),
Date::make('Diploma', 'earned_at'),
Text::make('Opmerking', 'note'),
];
}),
];
}
}
class User extends Resource
{
public function fields(Request $request)
{
return [
BelongsToMany::make('Registers', 'Specialisms', Specialism::class)
->hideFromIndex()
->fields(function() {
return [
Boolean::make('Goedgekeurd', 'approved'),
File::make('Diploma', 'diploma'),
File::make('Cijferlijst', 'cijferlijst'),
File::make('Overige', 'certificate'),
Date::make('Geldig', 'valid_until'),
Date::make('Diploma', 'earned_at'),
Text::make('Opmerking', 'note'),
];
}),
];
}
}
And this data is actually shown below the resource:
attached resource
but the file fields show just a dash instead of a link to the real file.
The other option to view the data would be the eye icon, but this just shows the specialism resource if you were on the user resource and vice versa not the actual specialism_user.
Maybe it's the way the path is stored, but if I click the edit button the path seems to be there. This is where it's stored:
if (request()->hasFile('certificaat')) {
$path = request()->file('certificaat')->store("other/{$user->id}", "public");
$connect->certificate = $path;
}
if (request()->hasFile('diploma')) {
$diploma = request()->file('diploma')->store("diplomas/{$user->id}", "public");
$connect->diploma = $diploma;
}
if (request()->hasFile('cijferlijst')) {
$cijferlijst = request()->file('cijferlijst')->store("cijferlijsten/{$user->id}", "public");
$connect->cijferlijst = $cijferlijst;
}
if ($connect->isDirty()) {
$connect->save();
}
And finally these are the models:
class User extends Authenticatable implements MustVerifyEmail
{
public function specialisms()
{
return $this
->belongsToMany(Specialism::class)
->using(UserSpecialism::class, 'specialism_user')
->withPivot(['valid_until','earned_at', 'note', 'approved', 'diploma', 'cijferlijst', 'certificate', 'school_id', 'id'])
->withTimestamps();
}
}
class Specialism extends Model
{
public function users()
{
return $this
->belongsToMany(User::class)
->using(UserSpecialism::class, 'specialism_user')
->withPivot(['valid_until','earned_at', 'note', 'approved', 'diploma', 'cijferlijst', 'certificate', 'school_id', 'id'])
->withTimestamps();
}
}
I hope somebody can point me in the right direction, because it feels like I've been running in circles trying the same things for ages :D

Did you try the displayUsing method (https://nova.laravel.com/docs/3.0/resources/fields.html#field-resolution-formatting) on the File field?
BelongsToMany::make('Registers', 'Specialisms', Specialism::class)
->fields(function () {
return [
// ...
File::make('certificate')
->displayUsing(function ($certificatePath) {
return 'Open file';
})->asHtml(),
// ...
];
})

Related

Laravel: prevent changing other users' items

I have three models. I want to avoid that users can change the todo's from todolists belonging to other users.
class User extends Authenticatable
{
public function todolists()
{
return $this->hasMany('App\Todolist');
}
public function todos()
{
return $this->hasManyThrough('App\Todo', 'App\Todolist');
}
}
class Todolist extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
public function todos()
{
return $this->hasMany('App\Todo');
}
}
class Todo extends Model
{
protected $casts = [
'completed' => 'boolean',
];
public function todolist()
{
return $this->belongsTo('App\Todolist');
}
}
To avoid users can view other users' todolists and todo items, I have implemented the following:
public function getTodosForTodolist(Todolist $todolist)
{
if (Auth::user()->id == $todolist->user_id) {
$todos = Todo::where('todolist_id', $todolist->id )->get();
return view('todo/index', ['todos' => $todos);
}
else {
abort(403, 'Unauthorized action.');
}
}
Next step is to prevent that users can edit other users' todo items. Currently in the TodoController I have simply the following:
public function edit(Todo $todo)
{
if (Auth::user()->todos->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
}
This gives the following error:
Property [id] does not exist on this collection instance.
The error is because the current user has multiple todos. So I changed my code as follows.
public function edit(Todo $todo)
{
if (Auth::user()->todos->first()->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
abort('403', 'Unauthorized action.');
}
This works but it just feels very wrong to do this as such.
What would be a better way to accomplish that users' can view/edit/delete items belonging to other users?
I suggest that you use policies for your Todo and TodoList models and a scope to restrict todos to one user to prevent duplicated code within your app:
class ToDoListPolicy
{
public function view(User $user, TodoList $post)
{
return $user->id === $todolist->user_id;
}
}
class ToDoPolicy
{
public function edit(User $user, Todo $toDo)
{
$toDo->loadMissing('todolist');
return $user->id === $toDo->todolist->user_id;
}
}
Register them in your AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
TodoList::class => ToDoListPolicy::class,
Todo::class => ToDoPolicy::class
];
}
and then use them in your actions:
public function getTodosForTodolist(Todolist $toDoList)
{
$this->authorize('view', $toDoList);
$toDoList->loadMissing('todos');
return view('todo.index', ['todos' => $toDoList->todos);
}
class ToDoController extends Controller
{
public function edit(Todo $toDo)
{
$this->authorize('edit', $toDo);
return view('todo.edit', compact('toDo'));
}
}
And a scope to restrict the query to a specific user:
class Todo extends Model {
// ...
public function scopeByUser(Builder $query, ?User $user = null)
{
if (! $user) {
$user = Auth::user();
}
$query->whereHas('todolist', function (Builder $toDoListQuery) use ($user) {
$toDoListQuery->where('user_id', $user->id);
});
}
}
Answer to your questions in the comments.
Q1: I had to put Auth::user()->can('view', $todolist); in an if-else clause for it to work. Guess this is the way it works?
Q2: what is the difference between $this->authorize('edit', $todo) and Auth::user()->can('edit', $todo)?
Sorry, that was a mistake on my side. Auth::user()->can() returns a boolean whereas $this->authorize() (which is a method of the AuthorizesRequests trait usually included in the BaseController) throws an exception if the authorization failed.
If you want to let each user work only with his/her own Todos then adding a Global Scope is what you are looking for. This implementation will let your application feel that Todos ( of users other than the logged one ) does not exist.
Global Scopes can be used for many models which means it will reduce boiler plate code.
https://laravel.com/docs/7.x/eloquent#global-scopes

Populating a pivot table with Laravel/Eloquent

I have 8 tables: products, pests, actives, crops, active_product, pest_product, crop_product, and active_pest
I've built a form that loads information about a selected (agrichemical) product - in that form, the user selects the pests, actives, and crops associated with that product. When submitted, my existing code is saving the expected information in the products table and, through a set of "belongsToMany" relationships, the active_product, pest_product, and crop_product pivot tables are also correctly updated.
My problem is that I do not know how to use the actives and pests information (i.e. their respective id values) to add to/update the active_pest table.
I'd appreciate some direction.
The methods in my models are as follow:
product
public function Actives()
{
return $this->hasMany('App\Models\Active','active_product', 'product_id', 'active_id');
}
public function pest()
{
return $this->belongsToMany('App\Models\Pest','pest_product', 'product_id', 'pest_id');
}
public function active()
{
return $this->belongsToMany('App\Models\Active','active_product', 'product_id', 'active_id');
}
active
public function product()
{
return $this->belongsToMany('App\Models\Product', 'active_product', 'active_id', 'product_id');
}
public function pest()
{
return $this->belongsToMany('App\Models\Pest', 'active_pest', 'active_id', 'pest_id');
}
pest
public function active()
{
return $this->belongsToMany('App\Models\Active', 'active_pest', 'pest_id', 'active_id');
}
public function product()
{
return $this->belongsToMany('App\Models\Product','pest_product', 'pest_id', 'product_id');
}
public function crop()
{
return $this->belongsToMany('App\Models\Crop','crop_pest', 'pest_id', 'crop_id');
}
I am using BackPack for Laravel - my Product controller contains this function for updating:
public function update(UpdateRequest $request)
{
$redirect_location = parent::updateCrud($request);
return $redirect_location;
}
updateCrud is
public function updateCrud(UpdateRequest $request = null)
{
$this->crud->hasAccessOrFail('update');
$this->crud->setOperation('update');
// fallback to global request instance
if (is_null($request)) {
$request = \Request::instance();
}
// update the row in the db
$item = $this->crud->update($request->get($this->crud->model->getKeyName()),
$request->except('save_action', '_token', '_method', 'current_tab', 'http_referrer'));
$this->data['entry'] = $this->crud->entry = $item;
// show a success message
\Alert::success(trans('backpack::crud.update_success'))->flash();
// save the redirect choice for next time
$this->setSaveAction();
return $this->performSaveAction($item->getKey());
}
Thanks, Tom
you can use laravel's attach method like this:
$actives = App\Active::create([
'someColumn' => 'test',
'anotherColumn' => 'test',
]);
$pests = App\Pest::create([
'someColumn' => 'test',
'anotherColumn' => 'test',
]);
$actives->pest()->attach($pests);
^^^-relation name in model

How to add own data to POST in YII2?

i've question about adding some data outside form and send it with form data. Look! I have 3 fields ActiveForm:
name (text)
email (email)
course (hidden)
Ok, but i need to add one more named "status". I do not want to add hidden fields, just want to add inside controller or model.
How?
Controller:
public function actionFree()
{
$model = new SubscribeForm();
$this->view->title = "ШКОЛА ПИСАТЕЛЬСКОГО МАСТЕРСТВА: Новичок курс";
if ($post = $model->load(Yii::$app->request->post())) {
if ($model->save()) {
Yii::$app->session->setFlash('success', 'Данные приняты');
return $this->refresh();
}
else {
Yii::$app->session->setFlash('error', 'Ошибка');
}
}
else {
// страница отображается первый раз
return $this->render('free-course', ['model' => $model, 'course_id' => 1]);
}
}
Model:
class SubscribeForm extends ActiveRecord
{
public $fio;
public $email;
public $course;
public $status;
public static function tableName()
{
return 'users';
}
public function rules()
{
return [
// username and password are both required
[['fio', 'email'], 'required'],
[['email'], 'unique'],
['email', 'email'],
['email', 'safe']
];
}
}
You could just set the value in your controller, like this:
public function actionFree()
{
$model = new SubscribeForm();
$model->status = 'your-status-value';
// ... the rest of your code
Or you could add a default value in your model. This way you can still overrule the value from the controller or a form field, but will get this value when nothing else is supplied.
public function rules()
{
return [
['status', 'default', 'value' => 'your-default-status-value'],
// .. other rules
];
}

Laravel Nova + Spatie Media library

Trying to use Laravel Nova with Spatie Media Library. I created upload field like this:
Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
}),
Seams ok, but Nova still trying to save file name to "logo" column in manufacturers table.
Original excample to customize this field was:
File::make('Attachment')
->store(function (Request $request, $model) {
return [
'attachment' => $request->attachment->store('/', 's3'),
'attachment_name' => $request->attachment->getClientOriginalName(),
'attachment_size' => $request->attachment->getSize(),
];
})
I found a work around by setting an empty mutator on the model. In your case it would be:
class Manufacturer extends Model implements HasMedia
{
use HasMediaTrait;
public function setLogoAttribute() {}
//...
}
Here's an example of my entire implementation. Note that currently with Nova 1.0.6, the preview() method is not working, it's returning the thumbnail() url.
App/GalleryItem
class GalleryItem extends Model implements HasMedia
{
use HasMediaTrait;
public function setImageAttribute() {}
public function registerMediaConversions(Media $media = null)
{
$this->addMediaConversion('thumbnail')
->fit(Manipulations::FIT_CROP, 64, 64);
$this->addMediaConversion('preview')
->fit(Manipulations::FIT_CROP, 636, 424);
$this->addMediaConversion('large')
->fit(Manipulations::FIT_CONTAIN, 1920, 1080)
->withResponsiveImages();
}
public function registerMediaCollections()
{
$this->addMediaCollection('images')->singleFile();
}
}
App/Nova/GalleryItem
class GalleryItem extends Resource
{
public static $model = 'App\GalleryItem';
public static $with = ['media'];
public function fields(Request $request)
{
return [
Image::make('Image')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('image')->toMediaCollection('images');
})
->preview(function () {
return $this->getFirstMediaUrl('images', 'preview');
})
->thumbnail(function () {
return $this->getFirstMediaUrl('images', 'thumbnail');
})
->deletable(false);
];
}
}
As with Nova 3 (and Laravel 8) you need to return true from the fillUsing or store method:
File::make('Attachment')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return true;
// This will tell nova that you have taken care of it yourself.
})
As soon as you return anything but true nova will assume, that it needs to save something to the database. This leads to an error if the field does not exist in db (as to expect with spatie-medialibrary) or it will overwrite your precious data if the field exists but serves another purpose.
Nova allows you to return true from the callback to indicate that the processing is complete and that it shouldn't set any attributes itself.
This is the code that runs the callback:
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
{
//...
$result = call_user_func($this->storageCallback, $request, $model);
if ($result === true) {
return;
}
if (! is_array($result)) {
return $model->{$attribute} = $result;
}
foreach ($result as $key => $value) {
$model->{$key} = $value;
}
}
So either true or any empty array will achieve the same thing, but personally feels clearer to do the former.
Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return [];
}),
Maybe returning an empty array prevent nova from saving the name.

Laravel API ResourceCollection WhenLoaded

I try to include a relationship in my resource array if it has been eager loaded, but don't get it working.
Anyone has an idea, how I can check the relationships in the ResourceCollection?
Database schema looks like this:
Here is my Post Model
class Post extends Model
{
function categories() {
return $this->belongsToMany('App\Category');
}
}
Here is my Category Model
class Category extends Model
{
function posts() {
return $this->belongsToMany('App\Post');
}
}
Here is my Post Controller
Class PostController extends Controller
{
public function index()
{
return new PostResourceCollection(Post::with("categories")->get());
}
}
Here is my Post ResourceCollection
class PostResourceCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection->transform(function($page){
return [
'type' => 'posts',
'id' => $page->id,
'attributes' => [
'name' => $page->title,
],
];
}),
//'includes' => ($this->whenLoaded('categories')) ? 'true' : 'false',
//'includes' => ($this->relationLoaded('categories')) ? 'true' : 'false',
];
}
}
Maybe too late, below solution is a workaround for this case:
return [
...,
'includes' => $this->whenLoaded('categories', true),
];
Loading custom attribute:
return [
...,
'includes' => $this->whenLoaded('categories', fn() => $this->categories->name),
];
You relationship is wrong, a post belongs to many categories while a category has many posts so change:
class Category extends Model
{
function posts() {
return $this->belongsToMany('App\Post', 'category_post');
}
}
to
class Category extends Model
{
function posts() {
return $this->hasMany('App\Post', 'category_post');
}
}
Now when you load the post you can load the categories also:
$posts = Post::with('categories')->get();
got it.. That was the missing piece. if anyone has a better solution for this it would be much appreciated.
foreach ($this->collection as $item) {
if ($item->relationLoaded('categories')) {
$included = true;
}

Resources