Nino Rosella

Joined

2,130 Experience
0 Lessons Completed
2 Questions Solved

Activity

Posted in Medium.com style URLs for username

Hi everyone,

I'm implementing a username system in my app, and I'm trying to make the URLs pretty. I'd rather not have anything like:

https://www.myapp.com/users/joebloggs
https://www.myapp.com/u/joebloggs

but rather

https://www.myapp.com/joebloggs

If I want the pretty version I'm going to have to whitelist a whole load of paths, which I'd rather not do. I noticed that Medium.com and producthunt.com do the following to solve this:

https://www.myapp.com/@joebloggs

How do I go about creating the same in a Rails 6 app? Pretty sure I have to namespace the routes, but unsure how to implement it exactly.

Thanks!
Nino

Posted in Deleting Comments In Nested Threads Discussion

If you're using Postgres you're likely to run into an issue with this solution due to the following columns being generated when creating the schema:

    t.bigint "user_id", null: false
    t.string "commentable_type", null: false
    t.bigint "commentable_id", null: false

This means when you delete a comment you get the following error:

ERROR:  null value in column "user_id" violates not-null constraint

The workaround that I've implemented is to add a deleted column that defaults to false on the Comments table.

This means we can set deleted to true:

comment.rb

  def destroy
    update(deleted: true)
  end

_comment.html.erb

<% if comment.deleted == true %>
    <h5 class="text-semibold">[Deleted]</h5>
    <p>[deleted]</p>
<% else %>
    <h5 class="text-semibold"><%= comment.user.name %> posted:</h5>
    <%= simple_format(comment.body) %>
<% end %>

The same thing also be done using an authorisation gem like Pundit, if anyone already has that integrated into their app.

Posted in Refactoring controllers to keep them RESTful

Thanks for sending that over 👍 That's an even cleaner solution.

Ok, so now my code looks like the following:

routes.rb:

namespace :books do
  resource :google_book, only: [:create]
end

resources :books

controllers/books/google_book.rb:

class Books::GoogleBooksController < ApplicationController
  def create
    @book = Book.new
    @book.authors.build

    isbn = Book.sanitised_isbn(params[:q])

    begin isbn
      @response = GoogleBook.new(isbn)
    rescue => e
      render 'books/google_books/try_again'
      nil
    end
  end
end

views/books/google_books/_form.html.erb:

<%= form_with url: books_google_book_path, local: true do %>
  <%= text_field_tag :q, "", placeholder: "ISBN-10 or ISBN-13" %>
  <%= submit_tag("Lookup book", name: nil) %>
<% end %>

"And what I found is that the freedom that gives you is that each controller now has its own scope with its own sets of filters that apply..." - DHH

Indeed.

Posted in Refactoring controllers to keep them RESTful

Got it working. Will post the answer here in case anyone has the same question in the future.

My _form.html.erb became:

<%= form_with url: google_book_path, local: true do %>
  <p><small>Enter the ISBN (excluding spaces and hyphens)</small></p>
  <div class="form-group">
    <%= text_field_tag :q, "", placeholder: "ISBN-10 or ISBN-13", required: true, class: "form-control" %>
  </div>
  <%= submit_tag("Lookup book", name: nil, class: "btn btn-dark") %>
<% end %>

And I was forgetting that I obviously needed the appropriate view, so I added /views/google_books/create.html.erb

I was then able to remove the submit_book method from BooksController and everything just feels much cleaner and organised.

Posted in Refactoring controllers to keep them RESTful

Hi gang

I have a feature where a user can query the Google Books API with an ISBN number and get the book as a response. I have this all working nicely, but I'd like to refactor my Books controller to keep it restful.

Currently, my code looks like this:
routes.rb:

resources :books do
  collection do
    post :submit_book
    get :try_again
  end
end

BooksController.rb:

###
def submit_book
  @book = Book.new
  @book.authors.build

  isbn = Book.sanitised_isbn(params[:q])

  begin isbn
    @response = GoogleBooks.new(isbn)
  rescue => e
    redirect_to try_again_books_path(isbn: params[:q])
    nil
  end
end
###

_form.html.erb

<%= form_tag({controller: "books", action: "submit_book", method: "post"}) do %>
  <%= text_field_tag :q, "", placeholder: "ISBN-10 or ISBN-13" %>
  <%= submit_tag("Lookup book", name: nil) %>
<% end %>

What I'm trying to do now is to extract the logic from the submit_book method into it's own controller, but I'm having difficulty making it work. I'm getting bogged down in the weeds for sure. Anyone got any pointers on where I should be going?

When I submit this new form, my browser downloads an empty file lol

routes.rb:

resource :google_book, only: [:create]
resources :books

google_books_controller.rb:

class GoogleBooksController < ApplicationController
  def create
    @book = Book.new
    @book.authors.build

    isbn = Book.sanitised_isbn(params[:q])

    begin isbn
      @response = GoogleBooks.new(isbn)
    rescue => e
      redirect_to try_again_books_path(isbn: params[:q])
      nil
    end
  end
end

_form.html.erb:

<%= form_tag({controller: "google_books", action: "create", method: "post"}) do %>
  <%= text_field_tag :q, "", placeholder: "ISBN-10 or ISBN-13" %>
  <%= submit_tag("Lookup book", name: nil) %>
<% end %>

Hi Chris,

I put <%= form.hidden_field :id %> into my form, and at first it didn't change anything. However, I commented-out the following lines in book.rb that find or create an Author, and the edit form works a charm, so thanks for your help there.

def book_authors_attributes=(book_author_attributes)
  book_author_attributes.values.each do |author_attribute|
    author = Author.find_or_create_by(name: author_attribute["author_attributes"]["name"])
    self.authors << author
  end
end

My question now is how might I be able to make the above code work? It works great on the new form, but I get duplicates again when I use it for editing. I think the problem lies with the self.authors << author part.

I'm guessing that this line is reinserting all the authors back into the form after I've deleted them and causing the duplicates once more?

Hi team,

I've been following along with the Dynamic Nested Forms with Stimulus JS lesson, but I've been attempting to adapt it for a has_many :through relationship.

So far, I've managed to get the new and create methods working just fine. However, I'm really having trouble with editing. I've posted my code below.

When I edit a Book, I'm getting duplicate records created.

For instance, if I try to edit a Book that already has two Authors, then it adds these two authors plus any extras that I add during the edit. This then compounds and before I know it I have a Book with many, many Authors.

Anyone got a clue what's happening?

EDIT: Here's a sample app with the code

book.rb

class Book < ApplicationRecord
    has_many :book_authors, inverse_of: :book, dependent: :destroy
    has_many :authors, through: :book_authors

    validates_presence_of :title, :description

    accepts_nested_attributes_for :book_authors, reject_if: :all_blank, allow_destroy: true

    def book_authors_attributes=(book_author_attributes)
    book_author_attributes.values.each do |author_attribute|
      author = Author.find_or_create_by(name:author_attribute["author_attributes"]["name"])
      self.authors << author
    end
  end
end

author.rb

class Author < ApplicationRecord
    has_many :books, through: :book_authors
    has_many :book_authors, dependent: :destroy
end

book_author.rb

class Author < ApplicationRecord
    has_many :books, through: :book_authors
    has_many :book_authors, dependent: :destroy
end

books_controller.rb

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  def new
    @book = Book.new
    @book.book_authors.build.build_author
  end

  def edit
  end

  def create
    @book = Book.new(book_params)

    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: 'Book was successfully created.' }
        format.json { render :show, status: :created, location: @book }
      else
        format.html { render :new }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @book.update(book_params)
        format.html { redirect_to @book, notice: 'Book was successfully updated.' }
        format.json { render :show, status: :ok, location: @book }
      else
        format.html { render :edit }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end


  private
    def set_book
      @book = Book.find(params[:id])
    end

    def book_params
      params.require(:book).permit(:title, :description, book_authors_attributes: [:id, :author_id, :_destroy, author_attributes: [:id, :_destroy, :name]])
    end
end

_form.html.erb

<%= form_for @book do |form| %>
  <% if @book.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:</h2>

      <ul>
      <% @book.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.text_field :description %>
  </div>

  <div data-controller="nested-form">
    <template data-target="nested-form.template">
      <%= form.fields_for :book_authors, child_index: "NEW_RECORD" do |book_author| %>
        <%= render "book_author_fields", form: book_author %>
      <% end %>
    </template>

    <%= form.fields_for :book_authors do |book_author| %>
      <%= render "book_author_fields", form: book_author %>
    <% end %>

    <div data-target="nested-form.links">
      <%= link_to "add author", "#", data: { action: "nested-form#add_association" } %>
    </div>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

_book_author_fields.html.erb

<%= content_tag :div, class: "nested-fields", data: { new_record: form.object.new_record? } do %>
    <div class="nested-field-input d-flex justtify-content-between mb-2">
        <div class="col-11 pl-0">
            <%= form.fields_for(:author) do |author_form| %>
        <%= author_form.text_field :name %>
      <% end %>
        </div>
        <div class="col-1">
            <%= link_to "delete", "#", data: { action: "nested-form#remove_association" } %>
    </div>
    </div>
    <%= form.hidden_field :_destroy, as: :hidden %>
<% end %>

nested_form_controller.js

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "links", "template" ]

  connect() {

  }

  add_association(event) {
    event.preventDefault()

    var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
    this.linksTarget.insertAdjacentHTML('beforebegin', content)
  }

  remove_association(event) {
    event.preventDefault()

    let wrapper = event.target.closest(".nested-fields")
    if(wrapper.dataset.newRecord == "true") {
        wrapper.remove()
    } else {
        wrapper.querySelector("input[name*='_destroy']").value = 1
        wrapper.style.display = "none"
    }
  }
}

Posted in Rails credentials returning nil

Ok so I found the following: https://github.com/rails/rails/pull/33521

I've now run rails credentials:edit --environment development and added the credentials in that file, but I'm still having the same issue when running a number of commands:

Rails.application.credentials[:development][:secret_key_base]
Rails.application.credentials.development.secret_key_base

I'm likely doing something wrong though...

Posted in Rails credentials returning nil

Hi Chris. Do you have any further info on that? Just tried looking through the release notes but can't find anything.

Posted in Rails credentials returning nil

I have a Rails 6.0.0beta2 app and I'm having trouble reading my credentials.

When I do rails credentials:edit I can add credentials and they're saving just fine.

aws:
  access_key_id: 123
  secret_access_key: 345

However, when I run a console and try reading the credentials I get the following:

Rails.application.credentials[:aws]
  => nil 

or

Rails.application.credentials.aws[:access_key_id]
Traceback (most recent call last):
    1: from (irb):2
NoMethodError (undefined method `[]' for nil:NilClass)

or

Rails.application.credentials[:aws][:access_key_id]
Traceback (most recent call last):
    1: from (irb):3
NoMethodError (undefined method `[]' for nil:NilClass)

No matter what I try my credentials always seem to be nil.

If I run Rails.application.credentials.secret_key_base I get the correct result, so the file can be read just fine.

Posted in Dynamic Nested Forms with Stimulus JS Discussion

I included these revisions and the code still works fine, and is now more readable.

Thanks for sharing.

I mananged to solve this. Here's what I did for anyone that comes across this in the future. Note in the new action I have @note.build_chapter rather than @note.chapter.build

class Book < ApplicationRecord
    has_many :chapters
    has_many :notes
end
class Chapter < ApplicationRecord
    belongs_to :book
    has_many :notes
end
class Note < ApplicationRecord
    belongs_to :chapter, optional: true
  accepts_nested_attributes_for :chapter
end
class NotesController < ApplicationController
    def new
        @note = Note.new
        @note.build_chapter
    end

    def create
        @book = Book.find(params[:book_id])
        @note = @book.notes.create(note_params)

        if params[:book_note][:chapter_name]
            parent_chapter = @book.chapters.find_or_create_by(name: params[:note][:chapter_name], book_id: params[:book_id])
            @note.chapter = parent_chapter
        end
    end

    private

    def book_note_params
        params.require(:book_note).permit(:title, :body,  : chapters_attributes: [:id, :name, :chapter_id, :_destroy])
    end

end

The controllers need refactoring, but you get the gist.

Hi team,

I've trying to model books, chapters, and notes.

I have the following:

class Book < ApplicationRecord
    has_many :chapters
    has_many :notes
end
class Chapter < ApplicationRecord
    belongs_to :book
    has_many :notes
end
class Note < ApplicationRecord
    belongs_to :chapter
end

I can create Books and notes just fine.

What I want to do when creating a new Note is either create a new Chapter or assign an existing one to a note. Said another way: I'm trying to create a parent from the child before the parent even exists, or assigning an exising parent to the child.

This is the kind of functionality that's provided by gems such as acts_as_taggable_on. I've tried using nested forms, but just couldn't get it near to what I wanted. I'm wondering if my architecture is even correct for this type of use? Any guidance you could provide would be much appreciated.

Yep, works a charm.

Thank you so much Dylan.
Hey Dylan,

Just want to thank you for going to the effort to build this! Love the GoRails community.

However, when I run that code and create a new book, but leave the author field blank, it doesn't seem to be hitting the validation. So I'm able to create a book with no author. Any ideas why this might be?

I have a Rails 5 app, and I'm having a problem with validations using the Cocoon gem with nested, dynamic forms. 

When a validation on my Book model fails, and my create action renders :new, the Authorfield disappears from my new book form.

One thing I've noticed is that when the validation fails, rather than passing authors back into my form, it's passing book_authors back because of the line @book.book_authors.build(author_id: author.id) in my create action. Pretty sure this is the cause of my issue, but don't know how to fix it.

books_controller.rb

class BooksController < ApplicationController
...

def new
@book = Book.new
@book.authors.build
end

def create
@book = current_user.books.create(book_params)
params[:book][:authors_attributes].each do |k,v|
author = Author.find_or_create_by(name: v['name'], user_id: current_user.id)
@book.book_authors.build(author_id: author.id)
end

if @book.save
    redirect_to book_path(@book)
else
  render :new
end

end

private

def book_params
params.require(:book).permit(:title, :published_city, :description, author_ids:[])
end

def author_params
params.require(:book).permit(authors_attributes: [:id, :name, :_destroy])
end
end

book.rb

class Book < ApplicationRecord

has_many :book_authors
has_many :authors, through: :book_authors
belongs_to :user

accepts_nested_attributes_for :authors, allow_destroy: true
validates :title, :published_city, presence: true
validates_associated :authors, inverse_of: :book
end

book_author.rb

class BookAuthor < ApplicationRecord
belongs_to :book
belongs_to :author
end
author.rb

class Author < ApplicationRecord
has_many :book_authors
has_many :books, through: :book_authors

validates :name, presence: true
end

new.html.erb

<%= form_for @book do |f| %>
<%= f.text_field :title, required: true %>
<%= f.text_area :description %>
<div id='authors'>
<%= f.fields_for :authors do |author| %>
<%= render 'author_fields', :f => author %>
<% end %>
<div class='links'>
<%= link_to_add_association 'Add another author', f, :authors %>
</div>
</div>
<%= f.text_field :published_city %>
<%= f.submit %>
<% end %>

So I managed to get it mostly working using:

class AccountMiddleware
    def initialize(app)
        @app = app
    end

    def call(env)
        _, username, request_path = env["REQUEST_PATH"].split('/', 3)

        if username
            Current.user = User.where(username: username)

            env["SCRIPT_NAME"]  = "/#{username}"
            env["PATH_INFO"]    = "/#{request_path}"
            env["REQUEST_PATH"] = "/#{request_path}"
            env["REQUEST_URI"]  = "/#{request_path}"
        end

        status, headers, body = @app.call(env)
        [status, headers, body]
    end
end
The problem here, of course, is that I have a problem with URLs such as 'users/sign_up/' where Rails thinks username is 'sign_up'


Hi Chris,

This is great, but just wondering how I might be able to use an account_name rather than the account_id?

So would like the URL to look like: sample.com/coolaccount/posts/1
Jacob, you nailed it.

Thanks so much for your help!