Ajouter des commentaires pour le blog Phoenix

Maintenant que le blog contient des articles avec une gestion des droits permettant aux auteurs de créer, modifier ou supprimer leurs articles, nous souhaitons permettre aux lecteurs d’ajouter des commentaires sous les articles du blog Phoenix

Cet article est la suite des articles sur la création du blog avec Phoenix :

Notre article sur Ecto et PostgreSQL permet aussi de mieux comprendre le code que nous avons dans cet article. Pour réaliser cet article, nous continuons la lecture de Productive Programmer, build a medium like blog clone with Phoeinix.

Ajoutons une table pour les commentaires du blog Phoenix

Nous ajoutons la table comments avec la commande mix phx.gen.Live :

  • cd C:\CarbonX1\Phoenix\Projets\blogset ‘se placer dans le dossier du projet
  • mix phx.gen.Live Comments Comment comments ‘Contexte, Module, table
  • message:text story_id:references:stories user_id:references:users ‘liste des champs

A chaque commentaire nous souhaitons avoir :

  • le texte du commentaire avec le champ message
  • le lien vers l’identifiant de l’article
  • le lien vers l’auteur du commentaire (il faut être identifier pour créer un commentaire)
C:\CarbonX1\Phoenix\Projets\blogset>mix phx.gen.Live Comments Comment comments message:text story_id:references:stories user_id:references:users
* creating lib/blogset_web/live/comment_live/show.ex
* creating lib/blogset_web/live/comment_live/index.ex
* creating lib/blogset_web/live/comment_live/form_component.ex
* creating lib/blogset_web/live/comment_live/index.html.heex
* creating lib/blogset_web/live/comment_live/show.html.heex
* creating test/blogset_web/live/comment_live_test.exs
* creating lib/blogset/comments/comment.ex
* creating priv/repo/migrations/20240126132811_create_comments.exs
* creating lib/blogset/comments.ex
* injecting lib/blogset/comments.ex
* creating test/blogset/comments_test.exs
* injecting test/blogset/comments_test.exs
* creating test/support/fixtures/comments_fixtures.ex
* injecting test/support/fixtures/comments_fixtures.ex

Add the live routes to your browser scope in lib/blogset_web/router.ex:

    live "/comments", CommentLive.Index, :index
    live "/comments/new", CommentLive.Index, :new
    live "/comments/:id/edit", CommentLive.Index, :edit

    live "/comments/:id", CommentLive.Show, :show
    live "/comments/:id/show/edit", CommentLive.Show, :edit


Remember to update your repository by running migrations:

    $ mix ecto.migrate


C:\CarbonX1\Phoenix\Projets\blogset>

Lors de la génération phx.gen.Live de Comments Cometn comments, nous avons créé des pages permettant de voir la liste ainsi que les pages d’édition et modification des commentaires. Nous avons aussi le module Comment. Le module Comment fait le lien avec la base de données depuis les pages affichant les commentaires.

Lors de l’execution, nous avons la liste des routes proposées pour chacune des pages commentaire. De fait, nous n’avons pas besoin des pages proposée, donc nous n’insérons pas les chermin dans le router.

Nous devons lancer la migration pour modifier la base de données :

  • mix ecto.migrate
C:\CarbonX1\Phoenix\Projets\blogset>mix ecto.migrate
Compiling 5 files (.ex)
warning: no route path for BlogsetWeb.Router matches "/comments"
  lib/blogset_web/live/comment_live/index.html.heex:39: BlogsetWeb.CommentLive.Index.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{assigns.comment}"
  lib/blogset_web/live/comment_live/show.html.heex:24: BlogsetWeb.CommentLive.Show.render/1

warning: no route path for BlogsetWeb.Router matches "/comments"
  lib/blogset_web/live/comment_live/index.html.heex:32: BlogsetWeb.CommentLive.Index.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{assigns.comment}"
  lib/blogset_web/live/comment_live/show.html.heex:17: BlogsetWeb.CommentLive.Show.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{comment}/edit"
  lib/blogset_web/live/comment_live/index.html.heex:20: BlogsetWeb.CommentLive.Index.render/1

warning: no route path for BlogsetWeb.Router matches "/comments"
  lib/blogset_web/live/comment_live/show.html.heex:15: BlogsetWeb.CommentLive.Show.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{comment}"
  lib/blogset_web/live/comment_live/index.html.heex:18: BlogsetWeb.CommentLive.Index.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{assigns.comment}/show/edit"
  lib/blogset_web/live/comment_live/show.html.heex:5: BlogsetWeb.CommentLive.Show.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/#{comment}"
  lib/blogset_web/live/comment_live/index.html.heex:13: BlogsetWeb.CommentLive.Index.render/1

warning: no route path for BlogsetWeb.Router matches "/comments/new"
  lib/blogset_web/live/comment_live/index.html.heex:4: BlogsetWeb.CommentLive.Index.render/1

Generated blogset app

14:29:17.425 [info] == Running 20240126132811 Blogset.Repo.Migrations.CreateComments.change/0 forward

14:29:17.427 [info] create table comments

14:29:17.467 [info] create index comments_story_id_index

14:29:17.470 [info] create index comments_user_id_index

14:29:17.480 [info] == Migrated 20240126132811 in 0.0s

C:\CarbonX1\Phoenix\Projets\blogset>

Le code généré contient maintenant un dossier BLOGSET/lib/blogset/comments. Dans ce dossier nous avons le fichier du module comment.ex avec le module Blogset.Comments.Comment qui gére le lien vers la table de base de données comments.

defmodule Blogset.Comments.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :message, :string
    
    #field :story_id, :id
    belongs_to :story, Blogset.Stories.Story
    
    #field :user_id, :id
    belongs_to :user, Blogset.Accounts.User

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:message])
    |> validate_required([:message])
  end
end

Créer les liens entre les tables dans le schema

Dans la partie shéma, nous voyons le champ story_id que nous avons demandé, et nous allons maintenant préciser que ce champ est un lien vers la table stories à travers le module Blogset.Stories.Story en remplaçant :

  • field :story_id, :id ‘à remplacer par
  • belongs_to :story, Blogset.Stories.Story ‘le lien :story avec le module de story.ex

Nous devons penser aussi à chaque utilisation de belong, à ajouter la réciproque dans le module visé avec la déclaration has_many. Nous montrerons cela plus bas dans l’article, lorsque nous mettrons en place la suppression des commentaires lors de la suppression de l’article.

Pour le champ user_id nous réalisons la même modification. Nous avons déjà réalisé changement équivalent dans Blogset.Stories.Story pour décrire le fait que l’article appartient à un auteur :

  • field :user_id, :id ‘à remplacer par
  • belongs_to :user, Blogset.Accounts.User ‘le lien :user avec le module de user.ex

Ajouter les commentaires sous les articles du blog Phoenix

Nous souhaitons ajouter les commentaires directement sous l’article du blog Phoenix. Dans les pages généré par Phoenix, nous avons le code souhaité dans un bloc modal dans le fichier index qui donne la liste des commentaires. Nous prenons le code du live_component pour le placer sous l’article dans la page en mode détail show.

BLOGSET/…/Comment/index.html.heex :

<.modal :if={@live_action in [:new, :edit]} id="comment-modal" show on_cancel={JS.patch(~p"/comments")}>
  <.live_component
    module={BlogsetWeb.CommentLive.FormComponent}
    id={@comment.id || :new}
    title={@page_title}
    action={@live_action}
    comment={@comment}
    patch={~p"/comments"}
  />
</.modal>

Et nous plaçons ce code sous l’article dans un div avec une séparation de 20 pixels grâce à la classe Tailwind mt-20. (ligne 12 à 21 ci-dessous)

BLOGSET/…/Story/show.html.heex :

<.header>
  Story <%= @story.id %>
  <:subtitle>This is a story record from your database.</:subtitle>
</.header>

<.list>
  <:item title="Name"><%= @story.user.name %></:item>
  <:item title="Title"><%= @story.title %></:item>
  <:item title="Body"><%= @story.body %></:item>
</.list>

<div class="mt-20">
  <.live_component
    module={BlogsetWeb.CommentLive.FormComponent}
    id={@comment.id || :new}
    title={@page_title}
    action={@live_action}
    comment={@comment}
    patch={~p"/comments"}
  />
</div>

<.back navigate={~p"/"}>Back to stories</.back>

Pour que ce code qui affiche la page de création de commentaire, nous devons avoir une structure %Comment{} référencé par comment: comme clef (ligne 18). Nous l’ajoutons (ligne 5) dans

BLOGSET/…/Story/show.ex :

alias Blogset.Comments.Comment  #pour avoir accès à la structure Comment

def mount(_params, _session, socket) do
	#{:ok, socket}  #code standard généré
    {:ok, assign(socket, comment: %Comment{})}   #nous ajoutons la référence à Comment
end

La référence à Comment est nécessaire pour que le formulaire fonctionne, grace à la clef comment:

Nous pouvons voir notre page modifiée :

  • cd C:\CarbonX1\Phoenix\Projets\blogset ‘dossier du projet
  • code . ‘ouvrir vs code
  • iex -S mix phx.server ‘execution en mode interactif
  • http://localhost:4000/stories ‘page avec la liste des articles
  • http://localhost:4000/ ‘page avec la liste des articles
Ajouter des commentaires pour le blog Phoenix#1 - La zone création de commentaire sous l'article
Ajouter des commentaires pour le blog Phoenix#1 – La zone création de commentaire sous l’article

Nous devons maintenant ajouter l’action correspondant au boutons Save Comment.

Ajouter l’action pour « Save Comment »

Dans le live_component que nous venons de placer dans show.html.heex (voir ligne 17 ci-dessus), nous avons action={@live_action}. Regardons comment est construit le live_component appelé ici dans module={BlogsetWeb.CommentLive.FormComponent} (ligne 14) qui se trouve dans le fichier :

BLOGSET/lib/blogset_web/live/comment_live/form_component.ex :

defmodule BlogsetWeb.CommentLive.FormComponent do
  use BlogsetWeb, :live_component

  alias Blogset.Comments

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage comment records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="comment-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:message]} type="text" label="Message" />
        <:actions>
          <.button phx-disable-with="Saving...">Save Comment</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

Le boutons intitulé Save Comment est dans un formulaire <.simple_form></.simple_form> (ligne 15 à 26) pour lequel l’action est phx-submit= »save » (ligne 20). Nous devons donc regarder dans le code défini par handle_event(« save »,_, _).

Le code présent dans le form_component de Comment avec handle_event(« save »,_ ,_ ) en ligne 2 ci-dessous, nous montre que nous utilisons save_comment, et save_comment donne 2 options, :edit (ligne 5 à 18) et :new (ligne 20 à 33).

  def handle_event("save", %{"comment" => comment_params}, socket) do
    save_comment(socket, socket.assigns.action, comment_params)
  end

  defp save_comment(socket, :edit, comment_params) do
    case Comments.update_comment(socket.assigns.comment, comment_params) do
      {:ok, comment} ->
        notify_parent({:saved, comment})

        {:noreply,
         socket
         |> put_flash(:info, "Comment updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp save_comment(socket, :new, comment_params) do
    case Comments.create_comment(comment_params) do
      {:ok, comment} ->
        notify_parent({:saved, comment})

        {:noreply,
         socket
         |> put_flash(:info, "Comment created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

Comme nous souhaitons seulement créer des nouveaux commentaires et ne pas modifier les commentaires dans cette page, nous modifions le live_component de show ainsi :

  • ligne 15, nous laissons seulement :new et nous supprimons @comment.id || qui serait utilisé pour pouvoir modifier un commentaire.
  • lligne 17, action devient :new, car nous laissons seulement la création des commentaires, donc nous n’avons pas plusieurs option d’action.
  • ligne 19, nous mettons le chemin vers la page courante soit stories/#{@story.id}.

BLOGSET/…/Story/show.html.heex (à comparer avec la version précédente au dessus) :

<.header>
  Story <%= @story.id %>
  <:subtitle>This is a story record from your database.</:subtitle>
</.header>

<.list>
  <:item title="Name"><%= @story.user.name %></:item>
  <:item title="Title"><%= @story.title %></:item>
  <:item title="Body"><%= @story.body %></:item>
</.list>

<div class="mt-20">
  <.live_component
    module={BlogsetWeb.CommentLive.FormComponent}
    id={:new}
    title={@page_title}
    action={:new}
    comment={@comment}
    patch={~p"/stories/#{@story.id}"}
  />
</div>

<.back navigate={~p"/"}>Back to stories</.back>

Nous pouvons maintenant tester pour voir si la création du commentaire passe sans erreur.

Ajouter des commentaires pour le blog Phoenix#2 - Enregistrement du commentaire
Ajouter des commentaires pour le blog Phoenix#2 – Enregistrement du commentaire

Vérification du résultat dans la base de données

Regardons les modifications dans la base de données :

  • cd C:\CarbonX1\Phoenix\Projets\blogset ‘se placer dans le dossier du projet
  • psql -U postgres ‘se connecter en tant qu’utilisateur username: postgres
  • \c ‘pour vérifier la connexion
  • \c blogset_dev ‘ pour se connecter à la base blogset_dev
  • \d ‘ liste les tables de la base blogset_dev
  • \d users ‘ liste les colonnes de la table users
  • select * from users; ‘liste les données de la table users
  • select * from comments; ‘liste les données de la table comments
  • \q ‘pour quitter postgres
C:\CarbonX1\Phoenix\Projets\blogset>psql -U postgres
Mot de passe pour l'utilisateur postgres :
psql (16.1)
Attention : l'encodage console (850) diffère de l'encodage Windows (1252).
            Les caractères 8 bits peuvent ne pas fonctionner correctement.
            Voir la section « Notes aux utilisateurs de Windows » de la page
            référence de psql pour les détails.
Saisissez « help » pour l'aide.

postgres=# \c blogset_dev
Vous êtes maintenant connecté à la base de données « blogset_dev » en tant qu'utilisateur « postgres ».
blogset_dev=# \d
                  Liste des relations
 SchÚma |         Nom         |   Type   | PropriÚtaire
--------+---------------------+----------+--------------
 public | comments            | table    | postgres
 public | comments_id_seq     | sÚquence | postgres
 public | schema_migrations   | table    | postgres
 public | stories             | table    | postgres
 public | stories_id_seq      | sÚquence | postgres
 public | users               | table    | postgres
 public | users_id_seq        | sÚquence | postgres
 public | users_tokens        | table    | postgres
 public | users_tokens_id_seq | sÚquence | postgres
(9 lignes)


blogset_dev=# select * from comments;
 id |             message             | story_id | user_id |     inserted_at     |     updated_at
----+---------------------------------+----------+---------+---------------------+---------------------
  1 | un commentaire pour l'article 3 |          |         | 2024-01-26 15:41:05 | 2024-01-26 15:41:05
(1 ligne)


blogset_dev=# \q

C:\CarbonX1\Phoenix\Projets\blogset>

Le commentaire est bien créé pour notre article 3. Nous devons maintenant ajouter les références à l’article et à l’auteur du commentaire. car nous voyons que les colonnes story_id et user_id sont vide.

Comment mettre en place les liens entre les tables

Nous avons des liens entre les tables :

  • le commentaire appartient à un article
  • le commentaire appartient à un utilisateur.

Nous allons définir avec Octo ces liens afin que :

  • nous puissions enregistrer l’identifiant de l’article et l’identifiant de l’utilisateur dans le commanetaire.
  • que la suppression d’un article supprime aussi tous les commentaires associés

Le lien à l’enregistrement du commentaire

Pour créer un lien vers l’article à l’enregistrement du commentaire, nous devons ajouter les éléments suivants :

Dans show.html.heex, nous avons le live_component CommentLive.FormComponent. Nous ajoutons la référence à l’article :

  • story_id={@story.id} ‘ajouter la référence à l’article (lligne 19)

BLOGSET/lib/blogset_web/live/story_live/show.html.heex :

<.header>
  Story <%= @story.id %>
  <:subtitle>This is a story record from your database.</:subtitle>
</.header>

<.list>
  <:item title="Name"><%= @story.user.name %></:item>
  <:item title="Title"><%= @story.title %></:item>
  <:item title="Body"><%= @story.body %></:item>
</.list>

<div class="mt-20">
  <.live_component
    module={BlogsetWeb.CommentLive.FormComponent}
    id={:new}
    title={@page_title}
    action={:new}
    comment={@comment}
    story_id={@story.id}
    patch={~p"/stories/#{@story.id}"}
  />
</div>

<.back navigate={~p"/"}>Back to stories</.back>

Dans le form_component, nous ajoutons un champ caché ayant pour nom story_id :

  • <.input field={@form[:story_id]} type= »hidden » value={@story_id} /> ‘ajout ligne 22

BLOGSET/lib/blogeset_web/live/comment_live/form_component.ex :

defmodule BlogsetWeb.CommentLive.FormComponent do
  use BlogsetWeb, :live_component

  alias Blogset.Comments

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage comment records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="comment-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:story_id]} type="hidden" value={@story_id} />
        <.input field={@form[:message]} type="text" label="Message" />
        <:actions>
          <.button phx-disable-with="Saving...">Save Comment</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

Nous vérifions que dans le module Blogset.Comments.comment au niveau du schema nous avons bien

  • field :story_id, :id ‘à remplacer par
  • belongs_to :story, BlogsetStories.Story

Dans la fonction changeset, nous ajoutons story_id pour cast ainsi que dans validation_required :

  • |> cast(attrs, [:message, :story_id])
  • |> validation_required ([:message, :story_id])

BLOGSET/lib/blogeset/comments/comment.ex :

defmodule Blogset.Comments.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :message, :string

    #field :story_id, :id
    belongs_to :story, Blogset.Stories.Story

    #field :user_id, :id
    belongs_to :user, Blogset.Accounts.User

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:message, :story_id])
    |> validate_required([:message, :story_id])
  end
end

Maintenant l’enregistrement de la référence à l’article dans le commentaire fonctionne :

C:\CarbonX1\Phoenix\Projets\blogset>psql -U postgres
Mot de passe pour l'utilisateur postgres :
psql (16.1)
Attention : l'encodage console (850) diffère de l'encodage Windows (1252).
            Les caractères 8 bits peuvent ne pas fonctionner correctement.
            Voir la section « Notes aux utilisateurs de Windows » de la page
            référence de psql pour les détails.
Saisissez « help » pour l'aide.

postgres=# \c blogset_dev
Vous êtes maintenant connecté à la base de données « blogset_dev » en tant qu'utilisateur « postgres ».
blogset_dev=# select * from comments;
 id |                 message                  | story_id | user_id |     inserted_at     |     updated_at
----+------------------------------------------+----------+---------+---------------------+---------------------
  1 | un commentaire pour l'article 3          |          |         | 2024-01-26 15:41:05 | 2024-01-26 15:41:05
  2 | un deuxiÞme commentaire pour l'article 3 |        3 |         | 2024-01-26 17:38:40 | 2024-01-26 17:38:40
(2 lignes)


blogset_dev=# \q

C:\CarbonX1\Phoenix\Projets\blogset>

Le lien à la suppression de l’article

Lorsque nous souhaitons supprimer un article, nous avons une contrainte lié à la base de données qui empêche l’action delete, tant que l’id de l’article est référencé » dans une autre table. Or la table comment contient maintenant la référence à l’article story_id.

Regardons quel message de sécurité est affiché par la base de données.

  • cd C:\CarbonX1\Phoenix\Projets\blogset ‘dossier du projet
  • code . ‘ouvrir vs code
  • iex -S mix phx.server ‘execution en mode interactif
  • http://localhost:4000/stories ‘page avec la liste des articles de l’auteur
  • http://localhost:4000/ ‘page avec la liste des articles public
Ajouter des commentaires pour le blog Phoenix#3 - Impossible de supprimer l'article
Ajouter des commentaires pour le blog Phoenix#3 – Impossible de supprimer l’article

Nous voyons l’erreur d’éxecution : « constraint error when attempting to delete struct: » (voir la ligne 6 ci-dessous) :

 BlogsetWeb.StoryLive.Index.handle_event/3, at: lib/blogset_web/live/story_live/index.ex:47
[debug] QUERY ERROR source="stories" db=7.1ms queue=0.5ms idle=1069.2ms
DELETE FROM "stories" WHERE "id" = $1 [3]
↳ BlogsetWeb.StoryLive.Index.handle_event/3, at: lib/blogset_web/live/story_live/index.ex:48
[error] GenServer #PID<0.649.0> terminating
** (Ecto.ConstraintError) constraint error when attempting to delete struct:

    * "comments_story_id_fkey" (foreign_key_constraint)

If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call `foreign_key_constraint/3` on your changeset with the constraint
`:name` as an option.

The changeset has not defined any constraint.

    (ecto 3.11.1) lib/ecto/repo/schema.ex:815: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
    (elixir 1.15.5) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2
    (ecto 3.11.1) lib/ecto/repo/schema.ex:799: Ecto.Repo.Schema.constraints_to_errors/3
    (ecto 3.11.1) lib/ecto/repo/schema.ex:780: Ecto.Repo.Schema.apply/4
    (ecto 3.11.1) lib/ecto/repo/schema.ex:571: anonymous fn/13 in Ecto.Repo.Schema.do_delete/4
    (blogset 0.1.0) lib/blogset_web/live/story_live/index.ex:48: BlogsetWeb.StoryLive.Index.handle_event/3
    (phoenix_live_view 0.20.2) lib/phoenix_live_view/channel.ex:487: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.2.1) c:/CarbonX1/Phoenix/Projets/blogset/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
    (phoenix_live_view 0.20.2) lib/phoenix_live_view/channel.ex:246: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 5.0.2) gen_server.erl:1077: :gen_server.try_handle_info/3
    (stdlib 5.0.2) gen_server.erl:1165: :gen_server.handle_msg/6
    (stdlib 5.0.2) proc_lib.erl:251: :proc_lib.wake_up/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-F67VJ6VqrNBIkANh", event: "event", payload: %{"event" => "delete", "type" => "click", "value" => %{"id" => 3}}, ref: "38", join_ref: "17"}
State: %{socket: #Phoenix.LiveView.Socket<id: "phx-F67VJ6VqrNBIkANh", endpoint: BlogsetWeb.Endpoint, view: BlogsetWeb.StoryLive.Index, parent_pid: nil, root_pid: #PID<0.649.0>, router: BlogsetWeb.Router, assigns: %{__changed__: %{}, page_title: "Listing Stories", current_user: #Blogset.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 4, name: "François MARTIN", email: "francois-martin@gmail.com", confirmed_at: nil, inserted_at: ~U[2024-01-10 15:34:29Z], updated_at: ~U[2024-01-29 09:59:31Z], ...>, flash: %{}, live_action: :index, story: nil, streams: %{__changed__: MapSet.new([]), stories: %Phoenix.LiveView.LiveStream{name: :stories, dom_id: #Function<3.112696910/1 in Phoenix.LiveView.LiveStream.new/4>, ref: "0", inserts: [], deletes: [], reset?: false, consumable?: false}, __configured__: %{}, __ref__: 1}}, transport_pid: #PID<0.637.0>, ...>, components: {%{}, %{}, 1}, topic: "lv:phx-F67VJ6VqrNBIkANh", serializer: Phoenix.Socket.V2.JSONSerializer, join_ref: "17", upload_names: %{}, upload_pids: %{}}
[debug] MOUNT BlogsetWeb.StoryLive.Index

Une des façons de résoudre cette contrainte est d’interdire la suppresion et de gérer une colonne indiquant « deleted » pour les articles et « deleted » pour les commentaires associés. Cette méthode fonctionne et oblige à créer des filtres pour ne plus montrer les articles supprimés. Néanmoins cette méthode laisse dans la table toutes les informations, puisqu’il s’agit d’une suppression fictive.

Mettons en place la suppression des articles et des commentaires associés au niveau du schema.

Dans le module Blogset.Comments.Comment, nous avons la référence à l’article dans le schema :

  • belongs_to :story, BlogsetStories.Story

Nous ajoutons dans le module Blogset.Stories.Story la référence croisée vers les commentaires :

  • alias Blogset.Comments.Comment ‘pour donner accès au module Comment
  • has_many :comments, Comment, on_delete: :delete_all ‘pour supprimer les commentaires

Nous pouvons vérifier dans l’application le fonctionnement :

  • cd C:\CarbonX1\Phoenix\Projets\blogset ‘dossier du projet
  • code . ‘ouvrir vs code
  • iex -S mix phx.server ‘execution en mode interactif
  • http://localhost:4000/stories ‘page avec la liste des articles
Ajouter des commentaires pour le blog Phoenix#4 - L'article peut maintenant être supprimé
Ajouter des commentaires pour le blog Phoenix#4 – L’article peut maintenant être supprimé

Afficher la liste des commentaires sous l’article

Nous voulons maintenant afficher la liste des commentaires sous l’article, c’est à dire dans la page show.html.heex.

Nous allons utiliser la liste pour afficher les commentaires : <.list><:item></:item></.list>. Cela se fait par l’utilisation de :for dans <:item></:item> (voir ligne 24 à 28 ci-dessous) :

  • <:item :for={comment <- @story.comments} ><%= comment.message %> </:item>

BLOGSET/lib/blogset_web/live/story_live/show.html.heex :

<.header>
  Story <%= @story.id %>
  <:subtitle>This is a story record from your database.</:subtitle>
</.header>

<.list>
  <:item title="Name"><%= @story.user.name %></:item>
  <:item title="Title"><%= @story.title %></:item>
  <:item title="Body"><%= @story.body %></:item>
</.list>

<div class="mt-20">
  <.live_component
    module={BlogsetWeb.CommentLive.FormComponent}
    id={:new}
    title={@page_title}
    action={:new}
    comment={@comment}
    story_id={@story.id}
    patch={~p"/stories/#{@story.id}"}
  />
</div>

<div class="mt-20">
  <.list>
    <:item :for={comment <- @story.comments} title="Comment"><%= comment.message %> </:item>
  </.list>
</div>

<.back navigate={~p"/"}>Back to stories</.back>

Pour que cela fonctionne nous devons précharger les commentaires dans l’article. Nous avons l’erreur :

  • ** (Protocol.UndefinedError) protocol Enumerable not implemented for #Ecto.Association.NotLoaded of type Ecto.Association.NotLoaded (a struct)

Dans Blogset.Stories.ex, nous avons get_story!(id), et c’est là que nous devons ajouter la référence aux commentaires en plus de la référence au user déjà présente.

  • def get_story!(id), do: Repo.get!(Story, id)  |> Repo.preload(:user) ‘à remplacer par ci-dessous
  • def get_story!(id), do: Repo.get!(Story, id) |> Repo.preload([:user, :comments])

Cela est possible parceque dans le schema de Story, nous avons indique has_may :comments, Comment. Cela permet d’avoir la référence aux commentaires associés à l’article.

Ajouter des commentaires pour le blog Phoenix#4 - Visualisation de la liste des commentaires
Ajouter des commentaires pour le blog Phoenix#4 – Visualisation de la liste des commentaires

Ajouter la référence à l’utilisateur dans la commentaire

Nous réalisons les mêmes étapes que pour associer l’article au commentaire. Afin d’avoir la référence à l’utilisateur qui poste un commentaire, nous devons afficher la partie rédaction des commentaires dans une zone protégée vérifiant que l’utilisateur est identifié. Cela est possible directement dans la page story_live/show.html.heex en encadrant la partie rédaction de commentaire avec :

  • <%= if @current_user do %>
  • <%= end %>

BLOGSET/lib/blogset_web/live/story_live/show.html.heex :

<.header>
  Story <%= @story.id %>
  <:subtitle>This is a story record from your database.</:subtitle>
</.header>

<.list>
  <:item title="Name"><%= @story.user.name %></:item>
  <:item title="Title"><%= @story.title %></:item>
  <:item title="Body"><%= @story.body %></:item>
</.list>

<%= if @current_user do %>
  <div class="mt-20">
    <.live_component
      module={BlogsetWeb.CommentLive.FormComponent}
      id={:new}
      title={@page_title}
      action={:new}
      comment={@comment}
      story_id={@story.id}
      current_user_id={@current_user.id}
      patch={~p"/stories/#{@story.id}"}
    />
  </div>
<%= end %>

<div class="mt-20">
 
  <.list>
    <:item :for={comment <- @story.comments} title="Comment"><%= comment.message %> </:item>
  </.list>
</div>

<.back navigate={~p"/"}>Back to stories</.back>

Cela n’a pas fonctionné, car nous n’avons pas la variable @current_user de définit. Nous devons dans le router, placer le chemin vers la page /stories/id après le :mount_current_user. Nous déplaçons la ligne 4 en 14 ci-dessous :

BLOGSET/lib/blogset_web/router.ex :

  scope "/", BlogsetWeb do
    pipe_through :browser

    #live "/stories/:id", StoryLive.Show, :show
  end

  scope "/", BlogsetWeb do
    pipe_through [:browser]

    delete "/users/log_out", UserSessionController, :delete

    live_session :current_user,
      on_mount: [{BlogsetWeb.UserAuth, :mount_current_user}] do
        live "/stories/:id", StoryLive.Show, :show
        live "/users/confirm/:token", UserConfirmationLive, :edit
        live "/users/confirm", UserConfirmationInstructionsLive, :new
    end
  end

Maintenant, nous avons bien le formulaire de création de commentaire visible uniquezment pour les utilisateur connecté.

Ajout de la référence à l’auteur du commentaire

Nous avons déjà prévu dans la base de données la colonne contenant l’auteur du commentaire. Nous devons maintenant ajouter dans le formulaire de création du commentaire la référence à l’utilisateur.

Dans <.live_component /> dans la page show.html.heex, nous ajoutons la référence à l’utilisateur @current_user (ligne 10).

BLOGSET/live/blogset_web/live/story_live/show.html.heex :

  <div class="mt-20">
    <.live_component
      module={BlogsetWeb.CommentLive.FormComponent}
      id={:new}
      title={@page_title}
      action={:new}
      comment={@comment}
      story_id={@story.id}
      current_user_id={@current_user.id}
      patch={~p"/stories/#{@story.id}"}
    />
  </div>

Et dans le form_componnet nous ajoutons un champ caché user_id.

BLOGSET/live/comment_live/form_component.ex :

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage comment records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="comment-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:user_id]} type="hidden" value={@current_user_id} />
        <.input field={@form[:story_id]} type="hidden" value={@story_id} />
        <.input field={@form[:message]} type="text" label="Message" />
        <:actions>
          <.button phx-disable-with="Saving...">Save Comment</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

Et pour que le champ user_id soit pris en compte dans la base de données, nous devons ajouter la prise en compte de celui ci dans la fonction changeset (ligne 18 à 22 ci-dessous) :

  • |> cast(attrs, [:message, :story_id, :user_id])
  • |> validation_required ([:message, :story_id, :user_id])

BLOGSET/lib/blogset/comments/comment.ex :

defmodule Blogset.Comments.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :message, :string

    #field :story_id, :id
    belongs_to :story, Blogset.Stories.Story

    #field :user_id, :id
    belongs_to :user, Blogset.Accounts.User

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:message, :story_id, :user_id])
    |> validate_required([:message, :story_id, :user_id])
  end
end

Vérification de la présence de l’auteur du commentaire dans la base de données

Nous pouvons maintenant créer des commentaires exclusivement si nous somme identifié. Et lorsque nous créons un commentaire, l’id user_id est enregistré dans la commentaire.

Afficher le nom de l’utilisateur qui a posté un commentaire

Maintenant que nous avons le user_id dans le commentaire, nous pouvons afficher son nom devant les commentaires affichés.

Dans l’affichage de la liste des commentaires, nous complétons

BLOGSET/live/blogset_web/live/story_live/show.html.heex :

  • <:item ….. title={« comment by #{if comment.user do comment.user.name else « Anon » end} »}>

Nous devons charger l’utilisateur associé au commentaire.

BLOGSET/lib/blogset/stories.ex :

  • def get_story!(id), do: Repo.get!(Story, id) |> Repo.preload([:user, :comments]) |> Repo.preload(comments: :user)

Si nous voulons autoriser les commentaires des personnes non identifiées, nous modifions le mount de show.ex pour utiliser la variable current_user_id qui sera à nil si il n’y a pas d’utilisateur :

def mount(_params, _session, socket) do
  current_user_id = if socket.assigns[:current_user] do
          socket.assigns[:current_user].id
      else 
          nil
  end
  {:ok, assign(socket, comment: %Comment{}, current_user_id: current_user_id)}
end

Et dans show.html.heex, au niveau du <.live_component /> nous modifions pour mettre

  • current_user_id={@current_user_id}

Dans le changeset de comment.ex nous avions forcé la validation de la présencee de :user_id pour le commentaire avec validate_required. Nous devons supprimer ce contrôle pour gérer les cas de commentaire anonyme.

On peut dans la page show.html.heex ajouter un test pour vérifier si @current_user_id est définit on non et afficher la possibilité de laisser un commentaire que pour les visiteurs authentifiés en encadrant le formulaire avec :

  • <%= if @current_usert_id do %>
  • <% end %>
Ajouter des commentaires pour le blog Phoenix#5 - La liste des commentaires avec le noms du rédacteur
Ajouter des commentaires pour le blog Phoenix#5 – La liste des commentaires avec le noms du rédacteur

Conclusion

Nous avons mis en place les commentaires pour le blog. Ces commentaires contiennent à la fois la référence à l’article pour être affichés sous l’article et la référence à l’auteur du commentaire pour pouvoir afficher son nom.

Si vous avez aimé l'article vous êtes libre de le partager :-)

Laisser un commentaire