Laravel 8

Créer un blog – les commentaires

Nous avons dans le précédent article codé tout ce qui concerne les utilisateurs. Maintenant on va traiter les commentaires qui sont intimement liés aux articles. Il y a une action particulière pour les commentaires : le premier commentaire d’un utilisateur n’est pas immédiatement publié, il faut qu’il soit approuvé par le rédacteur de l’article ou l’administrateur. Mais c’est la même chose pour les commentaires suivants tant que l’approbation n’a pas eue lieu. En fait c’est l’utilisateur qui est approuvé plutôt que ses commentaires. On va devoir tenir compte de cette particularité dans le tableau.

Vous pouvez télécharger le code final de cet article ici.

L’approbation des utilisateurs

On va s’occuper en premier de l’approbation des utilisateurs concernant les commentaires.

Les routes

On a besoin de deux routes :

Route::prefix('admin')->group(function () {

    Route::middleware('redac')->group(function () {

        ...

        // Users
        Route::name('users.valid')->put('valid/{user}', [BackUserController::class, 'valid']);
        Route::name('users.unvalid')->put('unvalid/{user}', [BackUserController::class, 'unvalid']);
    });

Ces routes sont accessibles pour les rédacteurs.

La première est pour approuver et la seconde l’inverse, on a le droit de changer d’avis.

Le contrôleur

Dans le contrôleur UserController dont on dispose déjà on ajoute les deux méthodes :

public function valid(User $user)
{
    $user->valid = true;
    $user->save();

    return response()->json();
}

public function unvalid(User $user)
{
    $user->valid = false;
    $user->save();

    return response()->json();
}

Ces méthodes seront utilisées en Ajax, on retourne une réponse JSON. La seule action est de mettre à jour la colonne valid dans la table. Je n’ai pas jugé utile de protéger ces deux méthodes. Un rédacteur pourrait envoyer une requête pour agir sur le commentaire de l’article d’un autre rédacteur. Mais comme les lecteurs peuvent laisser des commentaires sur tous les articles la protection aurait peu d’intérêt.

Les commentaires

Passons maintenant aux commentaires.

Les routes

Pour les routes c’est simple, on a une ressource et on va se passer de show, create et store. D’autre part on a besoin d’une route supplémentaire pour les nouveaux commentaires :

Route::prefix('admin')->group(function () {

    Route::middleware('redac')->group(function () {  
        ...
        // Comments
        Route::resource('comments', BackResourceController::class)->except(['show', 'create', 'store']);
        Route::name('comments.indexnew')->get('newcomments', [BackResourceController::class, 'index']); 
    });

La configuration

Les titres

Dans app/config/titles on ajoute les titres pour les commentaires :

'comments' => [
    'index'  => 'Comments',
    'indexnew' => 'New Comments',
    'edit'   => 'Comment Edit',
],

Le menu

Dans app/config/menu on ajoute les items pour les commentaires :

'Comments' => [
    'icon' => 'comment',
    'role'   => 'redac',
    'children' => [
        [
            'name'  => 'All comments',
            'role'  => 'redac',
            'route' => 'comments.index',
        ],
        [
            'name'  => 'New comments',
            'role'  => 'redac',
            'route' => 'comments.indexnew',
        ],
        [
            'name'  => 'fake',
            'role'  => 'redac',
            'route' => 'comments.edit',
        ],
    ],
],

On doit avoir du nouveau dans le menu :

Le tableau

On crée la Datatable :

php artisan datatables:make Comments

<?php

namespace App\DataTables;

use App\Models\Comment;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
use Illuminate\Support\Facades\Route;

class CommentsDataTable extends DataTable
{
    use DataTableTrait;

    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->editColumn('user', function($comment) {
                return $comment->user->name;
            })
            ->editColumn('approval', function($comment) {
                if($comment->user->valid) {
                    return $this->button(
                              'users.unvalid', 
                              $comment->user->id, 
                              'warning', 
                              __('Disapprove'), 
                              'thumbs-down',
                              'valid'
                          );
                } 
                return $this->button(
                          'users.valid', 
                          $comment->user->id, 
                          'success', 
                          __('Approve'), 
                          'thumbs-up',
                          'valid'
                      );
            })
            ->editColumn('post', function($comment) {
                return '<a href="' . route('posts.display', $comment->post->slug) . '" target="_blank">' .  $comment->post->title . '</a>';
            })
            ->editColumn('created_at', function ($comment) {
                return formatDate($comment->created_at) . __(' at ') . formatHour($comment->created_at);
            })
            ->editColumn('action', function ($comment) {
                return $this->button(
                          'comments.edit', 
                          $comment->id, 
                          'warning', 
                          __('Edit'), 
                          'edit'
                      ).  $this->button(
                          'comments.destroy', 
                          $comment->id, 
                          'danger', 
                          __('Delete'), 
                          'trash-alt', 
                          __('Really delete this comment?')
                      );
            })
            ->rawColumns(['approval','created_at', 'post', 'action'])
            ->setRowClass(function ($comment) {
                return $comment->user->valid ? '' : 'alert-warning';
            });
    }

    public function query(Comment $comment)
    {
        // Show only redactor posts comments
        $query = isRole('redac') ? 
            $comment->whereHas('post.user', function ($query) {
                $query->where('users.id', auth()->id());
            }) : 
            $comment->newQuery();

        if(Route::currentRouteNamed('comments.indexnew')) {
            $query->has('unreadNotifications');
        }

        return $query->with('user:id,name,valid', 'post:id,title,slug');
    }

    public function html()
    {
        return $this->builder()
                    ->setTableId('comments-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->dom('Blfrtip')
                    ->lengthMenu();
    }

    protected function getColumns()
    {
        return [
            Column::computed('user')->title(__('Author')),
            Column::computed('approval')->title(__('Approval'))->addClass('align-middle text-center'),
            Column::make('body')->title(__('Comment')),
            Column::computed('post')->title(__('Answer to')),
            Column::make('created_at')->title(__('Date')),
            Column::computed('action')->title(__('Action'))->addClass('align-middle text-center'),
        ];
    }

    protected function filename()
    {
        return 'Comments_' . date('YmdHis');
    }
}

Vous devriez maintenant avoir le tableau :

Pour le moment les seuls boutons qui fonctionnent sont ceux de la suppression.

L’approbation

Quand on clique sur le bouton d’approbation on doit envoyer une requête en Ajax au serveur, alors on va ajouter un peu de Javascript à la vue back.shared.index :

(() => {

    ...

    const validElement = async e => {
        e.preventDefault();
        fetch(e.target.getAttribute('href'), { 
            method: 'PUT',
            headers: headers
        })
        .then(response => {
            if (response.ok) {
                document.location.reload();             
            } else {
              Swal.fire({
                  icon: 'error',
                  title: '@lang('Whoops!')',
                  text: '@lang('Something went wrong!')'
              });  
            }
        });
    }

    ...

    // Set listeners
    window.addEventListener('DOMContentLoaded', () => {
        ...
        wrapper('table', 'click', validElement, `e.target.matches('[data-name="valid"]')`);
    });

})()

Pour repérer les boutons d’approbation on ajoute un attribut data-name qui nous permet de filtrer le click. On récupère l’url au niveau du bouton et on utilise fetch pour générer la requête. Au retour si c’est bon on recharge la page parce que le changement peut avoir un impact au niveau de plusieurs commentaires s’ils sont issus du même utilisateur et ça serait un peu lourd de traiter ça en Javascript. En cas d’erreur on affiche un message.

Le bouton a donc deux aspects possibles. D’autre part pour bien repérer les commentaires à approuver on met un bon fond jaune bien visible :

Au passage je me rends compte qu’il manque une traduction dans resources/lang/fr.json :

"Disapprove": "Désapprouver",

Modifier un commentaire

La validation

On ajoute la form request :

php artisan make:request Back\CommentRequest

On a un seul champ à traiter :

<?php

namespace App\Http\Requests\Back;

use Illuminate\Foundation\Http\FormRequest;

class CommentRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return ['body' => 'required|max:2000'];
    }
}

La vue

Pour la modification d’un commentaire on ajoute une vue :

@extends('back.layout')

@section('main')

    <form 
        method="post" 
        action="{{ route('comments.update', $comment->id) }}">
        @method('PUT')
        @csrf

        <div class="row">
          <div class="col-md-12">
                
                <x-back.validation-errors :errors="$errors" />

                @if(session('ok'))
                    <x-back.alert 
                        type='success'
                        title="{!! session('ok') !!}">
                    </x-back.alert>
                @endif

                <x-back.card
                    type='info'
                    :outline="true"
                    title=''>
                    <x-back.input
                        title='Contenu'
                        name='body'
                        :value='$comment->body'
                        input='textarea'
                        :required="true">
                    </x-back.input>
                </x-back.card>

                <button type="submit" class="btn btn-primary">@lang('Submit')</button>

              </div>
        </div>


    </form>

@endsection

On retrouve les composants que l’on a déjà bien utilisés.

Vérifiez que tout fonctionne bien : validation, modification…

Conclusion

Avec les commentaires on a traité toute la partie la plus délicate du blog. Il ne nous restera plus que les contacts, les pages et les liens sociaux et sans doute des choses que j’ai dû oublier au passage mais que les plus vigilants me signaleront…

 

Print Friendly, PDF & Email

19 commentaires

Laisser un commentaire