Nino Rosella
Joined
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
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.
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.
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.
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 Author
s, 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 Author
s.
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.
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.
Thank you so much 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?
class BooksController < ApplicationController
...def new
@book = Book.new
@book.authors.build
enddef 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)
endif @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:[])
enddef author_params
params.require(:book).permit(authors_attributes: [:id, :name, :_destroy])
end
end
class Book < ApplicationRecordhas_many :book_authors
has_many :authors, through: :book_authors
belongs_to :useraccepts_nested_attributes_for :authors, allow_destroy: true
validates :title, :published_city, presence: true
validates_associated :authors, inverse_of: :book
end
class BookAuthor < ApplicationRecord
belongs_to :book
belongs_to :author
end
class Author < ApplicationRecord
has_many :book_authors
has_many :books, through: :book_authorsvalidates :name, presence: true
end
<%= 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 %>
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
So would like the URL to look like: sample.com/coolaccount/posts/1
Thanks so much for your help!