From the translator: “ Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or become its followers.
In this part we will finish the routine work on the comments, then to move on to more interesting things.
At the moment, our application is based on:
The previous part ended with the creation of a small nice comments interface. With it, you can view a list of comments to posts and add new ones. But he still lacks some possibilities.
We start by setting up the authorization model, as prescribed by paragraphs 1 and 3.
Open the file web / controllers / post_controller.ex . Going down, you will see a bunch of authorization logic that we already have. Instead of adding a new one, let's fix this code a little, thus preparing it for reuse.
First, we transfer the authorization check to a private function:
defp is_authorized_user?(conn) do user = get_session(conn, :current_user) (user && (Integer.to_string(user.id) == conn.params["user_id"] || Pxblog.RoleChecker.is_admin?(user))) end
And then we turn to it from the authorize_user function:
defp authorize_user(conn, _opts) do if is_authorized_user?(conn) do conn else conn |> put_flash(:error, "You are not authorized to modify that post!") |> redirect(to: page_path(conn, :index)) |> halt end end
Run the tests and make sure that nothing is broken.
$ mix test Compiled web/controllers/post_controller.ex ....................................................................... Finished in 1.3 seconds (0.5s on load, 0.7s on tests) 71 tests, 0 failures Randomized with seed 501973
Wonderful! Then you need to add a new plug-in to set the authorization flag, which we will use in the template. To do this, take the function we just wrote:
# Add to the top of the controller with the other plug declarations plug :set_authorization_flag # Add to the bottom with the other plug definitions defp set_authorization_flag(conn, _opts) do assign(conn, :author_or_admin, is_authorized_user?(conn)) end
We can test the performance in two ways:
Instead of checking everything manually, let's write tests all the same (hey, you need to write tests anyway)!
Let's get rid of the initial test “Shows chosen resource”, in return for which we will add four new ones: when is the is_authorized_user function ? returns true for the author or administrator, and false for the guest or user who entered in error. We need to change the setup block, add an exit function and write some new tests. To do this, in the file test / controllers / post_controller_test.exs, we write:
setup do role = insert(:role) user = insert(:user, role: role) other_user = insert(:user, role: role) post = insert(:post, user: user) admin_role = insert(:role, admin: true) admin = insert(:user, role: admin_role) conn = build_conn() |> login_user(user) {:ok, conn: conn, user: user, other_user: other_user, role: role, post: post, admin: admin} end defp login_user(conn, user) do post conn, session_path(conn, :create), user: %{username: user.username, password: user.password} end defp logout_user(conn, user) do delete conn, session_path(conn, :delete, user) end
And then add a few tests to the new logic:
test "when logged in as the author, shows chosen resource with author flag set to true", %{conn: conn, user: user, post: post} do conn = login_user(conn, user) |> get(user_post_path(conn, :show, user, post)) assert html_response(conn, 200) =~ "Show post" assert conn.assigns[:author_or_admin] end test "when logged in as an admin, shows chosen resource with author flag set to true", %{conn: conn, user: user, admin: admin, post: post} do conn = login_user(conn, admin) |> get(user_post_path(conn, :show, user, post)) assert html_response(conn, 200) =~ "Show post" assert conn.assigns[:author_or_admin] end test "when not logged in, shows chosen resource with author flag set to false", %{conn: conn, user: user, post: post} do conn = logout_user(conn, user) |> get(user_post_path(conn, :show, user, post)) assert html_response(conn, 200) =~ "Show post" refute conn.assigns[:author_or_admin] end test "when logged in as a different user, shows chosen resource with author flag set to false", %{conn: conn, user: user, other_user: other_user, post: post} do conn = login_user(conn, other_user) |> get(user_post_path(conn, :show, user, post)) assert html_response(conn, 200) =~ "Show post" refute conn.assigns[:author_or_admin] end
Now run the tests again to make sure everything is still good:
$ mix test .......................................................................... Finished in 1.2 seconds (0.5s on load, 0.7s on tests) 74 tests, 0 failures Randomized with seed 206903
Wonderful! Now, when we are confident in the work of the flag, let's take the UI.
Let's start by changing the output of comments in the Post Show template ( web / templates / post / show.html.eex file):
<div class="comments"> <h2>Comments</h2> <%= for comment <- @post.comments do %> <%= render Pxblog.CommentView, "comment.html", comment: comment, author_or_admin: @conn.assigns[:author_or_admin], conn: @conn, post: @post %> <% end %> </div>
Notice how much we did in the render function. Initially, it was decided not to use @author_or_admin , since we cannot be sure that the parameter will be set. Instead, we used the more secure @ conn.assigns function. Then we need to pass the @conn and post paths to the helper.
Now the comment template will be able to read the flag created by us, so let's move on to editing it ( web / templates / comment / comment.html.eex file ). Here you need to do two things:
Let's start with the first step. At the top of the template, add the line:
<%= if @conn.assigns[:author_or_admin] || @comment.approved do %>
(Again, note the use of conn.assigns
instead of a simple @value
). At the bottom of the template, add:
<% end %>
Then find the Approve / Delete buttons and change the div containing them:
<div class="col-xs-4 text-right"> <%= if @conn.assigns[:author_or_admin] do %> <%= unless @comment.approved do %> <button class="btn btn-xs btn-primary approve">Approve</button> <% end %> <button class="btn btn-xs btn-danger delete">Delete</button> <% end %> </div>
The final version of the template should look like this:
<%= if @conn.assigns[:author_or_admin] || @comment.approved do %> <div class="comment"> <div class="row"> <div class="col-xs-4"> <strong><%= @comment.author %></strong> </div> <div class="col-xs-4"> <em><%= @comment.inserted_at %></em> </div> <div class="col-xs-4 text-right"> <%= if @conn.assigns[:author_or_admin] do %> <%= unless @comment.approved do %> <button class="btn btn-xs btn-primary approve">Approve</button> <% end %> <button class="btn btn-xs btn-danger delete">Delete</button> <% end %> </div> </div> <div class="row"> <div class="col-xs-12"> <%= @comment.body %> </div> </div> </div> <% end %>
Now try to open the post in two different browser windows: in one - by an authorized user, and in the other - by a guest. For the first, comments on the check and buttons will be shown, for the second - only approved comments.
We are very close to finalizing the comments, so let's document the last piece: we’ll configure the approve and delete buttons.
To make the delete button work, you need to make sure that the controller supports the delete action. Open the file web / controllers / comment_controller.ex and scroll down to the delete function. Right now, it returns only conn
, so you need to change it to something like this:
def delete(conn, %{"id" => id, "post_id" => post_id}) do post = Repo.get!(Post, post_id) |> Repo.preload(:user) Repo.get!(Comment, id) |> Repo.delete! conn |> put_flash(:info, "Deleted comment!") |> redirect(to: user_post_path(conn, :show, post.user, post)) end
And now you can write a test to delete comments. To do this, open the file test / controllers / comment_controller_test.exs . First of all, add the line alias Pxblog.Comment , if it was not there before. Then add a comment insert in the setup block:
setup do user = insert(:user) post = insert(:post, user: user) comment = insert(:comment) {:ok, conn: build_conn(), user: user, post: post, comment: comment} end
Finally, we write the test itself:
test "deletes the comment", %{conn: conn, post: post, comment: comment} do conn = delete(conn, post_comment_path(conn, :delete, post, comment)) assert redirected_to(conn) == user_post_path(conn, :show, post.user, post) refute Repo.get(Comment, comment.id) end
Now let's run the tests, everything should be green. It remains to do only the Delete button itself. Return to the file web / templates / comment / comment.html.eex and replace the Delete button code with the following one.
<%= link "Delete", method: :delete, to: post_comment_path(@conn, :delete, @post, @comment), class: "btn btn-xs btn-danger delete", data: [confirm: "Are you sure you want to delete this comment?"] %>
There are so many things to stay in more detail. First we set the text for the button. Then we tell her to use the DELETE method instead of GET. And then - where to send the command for deletion (you can use mix phoenix.routes to get a list of available routes). Next we define CSS classes and set a pop-up window with confirmation of the action.
That's all! Check out the functionality yourself. If the tests are green, then we are done with the work on the Delete button!
The implementation of the confirmation button will be a bit more complicated. We need to try to understand what the essence of the “approved” flag update is all about. Let's start by writing the code for the update function in the comment controller ( web / controllers / comment_controller.ex file):
def update(conn, %{"id" => id, "post_id" => post_id, "comment" => comment_params}) do post = Repo.get!(Post, post_id) |> Repo.preload(:user) comment = Repo.get!(Comment, id) changeset = Comment.changeset(comment, comment_params) case Repo.update(changeset) do {:ok, _} -> conn |> put_flash(:info, "Comment updated successfully.") |> redirect(to: user_post_path(conn, :show, post.user, post)) {:error, _} -> conn |> put_flash(:info, "Failed to update comment!") |> redirect(to: user_post_path(conn, :show, post.user, post)) end end
And we’ll write a check for changing the “approved” status in the file test / controllers / comment_controller_test.exs :
defp login_user(conn, user) do post conn, session_path(conn, :create), user: %{username: user.username, password: user.password} end test "updates chosen resource and redirects when data is valid and logged in as the author", %{conn: conn, user: user, post: post, comment: comment} do conn = login_user(conn, user) |> put(post_comment_path(conn, :update, post, comment), comment: %{"approved" => true}) assert redirected_to(conn) == user_post_path(conn, :show, user, post) assert Repo.get_by(Comment, %{id: comment.id, approved: true}) end
Restart the tests. Everything should be green! Now we need
change the “Approve” button so that clicking on it sends a request to the update function with the approve parameter set to true. Replace it in the web / templates / comment / comment.html.eex file with the following code:
<%= form_for @conn, post_comment_path(@conn, :update, @post, @comment), [method: :put, as: :comment, style: "display: inline;"], fn f -> %> <%= hidden_input f, :approved, value: "true" %> <%= submit "Approve", class: "btn btn-xs btn-primary approve" %> <% end %>
This will create a small form for the sole purpose of making the update button work. It is sent via PUT, displays the inline form, and sets the “approved” value to true through a hidden field. We are close to perfection, except that technically ANY can change / delete / confirm comments! We need to add authorization to the comment controller to keep the application safe!
Copy the authorize_user and is_authorized_user functions? from PostController to CommentController (in web / controllers / comment_controller.ex ), but with some changes. First we need to make sure that we are actually working with the expected post, so we will change authorize_user to set_post_and_authorize_user :
defp set_post(conn) do post = Repo.get!(Post, conn.params["post_id"]) |> Repo.preload(:user) assign(conn, :post, post) end defp set_post_and_authorize_user(conn, _opts) do conn = set_post(conn) if is_authorized_user?(conn) do conn else conn |> put_flash(:error, "You are not authorized to modify that comment!") |> redirect(to: page_path(conn, :index)) |> halt end end defp is_authorized_user?(conn) do user = get_session(conn, :current_user) post = conn.assigns[:post] user && (user.id == post.user_id) || Pxblog.RoleChecker.is_admin?(user)) end
And we will need to add a plug to the top of the controller:
plug :set_post_and_authorize_user when action in [:update, :delete]
Run the tests. We will get several errors depending on whether we are logged in before updating / deleting or not, so let's change our tests (in the file test / controllers / comment_controller_test.exs ):
test "deletes the comment when logged in as an authorized user", %{conn: conn, user: user, post: post, comment: comment} do conn = login_user(conn, user) |> delete(post_comment_path(conn, :delete, post, comment)) assert redirected_to(conn) == user_post_path(conn, :show, post.user, post) refute Repo.get(Comment, comment.id) end test "does not delete the comment when not logged in as an authorized user", %{conn: conn, post: post, comment: comment} do conn = delete(conn, post_comment_path(conn, :delete, post, comment)) assert redirected_to(conn) == page_path(conn, :index) assert Repo.get(Comment, comment.id) end test "updates chosen resource and redirects when data is valid and logged in as the author", %{conn: conn, user: user, post: post, comment: comment} do conn = login_user(conn, user) |> put(post_comment_path(conn, :update, post, comment), comment: %{"approved" => true}) assert redirected_to(conn) == user_post_path(conn, :show, user, post) assert Repo.get_by(Comment, %{id: comment.id, approved: true}) end test "does not update the comment when not logged in as an authorized user", %{conn: conn, post: post, comment: comment} do conn = put(conn, post_comment_path(conn, :update, post, comment), comment: %{"approved" => true}) assert redirected_to(conn) == page_path(conn, :index) refute Repo.get_by(Comment, %{id: comment.id, approved: true}) end
And now let's run our tests:
$ mix test Compiling 1 file (.ex) ........................................................................... Finished in 1.1 seconds 75 tests, 0 failures Randomized with seed 721237
That's all! All our buttons are configured and work, and we properly show / hide unconfirmed posts and actions, depending on whether the user is authorized or not!
Now we have a fully working part of the comments with a small admin panel. There are many other features that we can start adding here. For example, the system of live comments (for which we will take in the next part), the updated design of our blogging system (for which we will get into Brunch), and generally we will go over the code and clean / fix it a bit (for example, we now have a ton of duplicate code with user authentication)! I would also like to give users the opportunity to log in through third-party systems and add support for RSS feeds, import from other platforms, etc.
Source: https://habr.com/ru/post/323462/
All Articles