Jay Killeen

Joined

4,380 Experience
27 Lessons Completed
0 Questions Solved

Activity

Posted in Comments With Polymorphic Associations Discussion

To make it as commenters you'd need to set an alias on the users association on comments. Hope this helps. Sorry I don't have exact code for it.

Posted in Comments With Polymorphic Associations Discussion

@post.comments.users would work if you had your comments has_many: users association on your comment model.

Posted in Nested Comment Threads in Rails - Part 2 Discussion

Pundit is my preference for this type of functionality. I've followed this tutorial and added the appropriate pundit policies on posts and comments along the way. +1 to pundit :)

Posted in Nested Comment Threads in Rails - Part 3 Discussion

I have fixed this issue but it requires quite a few changes. First of all, the cause is that the ajax is only rerendering the partial, which means the nesting value is not being incremented. That much is fairly obvious.

To fix this, I moved the max_nesting into the Comment model as a class variable. ie def self.max_nesting 3 end. I then replace all references as Comment.max_nesting. You can then move that part of the logic into the comments helper.

The second fix was to take the nesting value for the comment and add it as a field on the Comment model. So you know that the @comment.nesting value is stored with the comment itself.

It is worth noting I have the paranoia/soft delete function set which I think has reduced my chances of the nesting becoming broken as comments are deleted.

In my comment controller, I am storing the comment nesting value through a Comment model method called set_nesting. This increments from the parent comment OR sets it to 1.

comments_helper.rb

  def reply_to_comment_id(comment, nesting)
    nesting = 1 unless nesting.present?
    max_nesting = Comment.max_nesting
    if max_nesting.blank? || nesting < max_nesting
      comment.id
    else
      comment.parent_id
    end
  end
end

comments_controller.rb

class CommentsController < ApplicationController
  before_action :authenticate_user!

  def create
    @comment = @commentable.comments.new(comment_params)
    @comment.nesting = @comment.set_nesting
    @comment.user = current_user
    if @comment.save
      respond_to do |format|
        format.html { redirect_to @commentable }
        format.js
      end
    else
      redirect_to @commentable, alert: "Something went wrong."
    end
  end

  def destroy
    @comment = @commentable.comments.find(params[:id])
    @comment.destroy
    redirect_to @commentable
  end

  def restore
    @comment = @commentable.comments.with_deleted.find(params[:id])
    @comment.restore
    redirect_to @commentable
  end

  private

    def comment_params
      params.require(:comment).permit(:body, :parent_id)
    end

end

comment.rb

class Comment < ApplicationRecord
  acts_as_paranoid
  belongs_to :user
  belongs_to :commentable, polymorphic: true
  belongs_to :parent, optional: true, class_name: "Comment"

  validates :body, presence: true
  validates_length_of :body, maximum: 140

  def comments
    Comment.with_deleted.where(commentable: commentable, parent_id: id).order(created_at: :asc)
  end

  def self.max_nesting
    3
  end

  def set_nesting
    if self.parent.present? && self.parent.nesting.present?
      self.nesting = self.parent.nesting + 1
    else
      self.nesting = 1
    end
  end

end

_comment.html.erb

<div class="border-gray-300 border-l p-4 my-4 mt-2 ml-2">
  <div class="flex"><%= comment.user.name %> says..</div>
    <% if comment.deleted? %>
      <div class="border-gray-300 border-l p-2 italic text-gray-500">
        <%= simple_format "This comment has since been deleted..." %>
        <div class="italic text-gray-500 text-sm">
          <div class="flex">
            <%= comment.created_at.strftime("%I:%M %p") %> • <%= comment.created_at.strftime("%d %b %y") %> <%= "~" + time_ago_in_words(comment.created_at) + " ago."%>
          </div>
        </div>
      </div>
    <% else %>
      <div class="border-gray-300 border-l p-2">
        <%= simple_format comment.body %>
        <div class="italic text-gray-500 text-sm">
          <div class="flex">
            <%= comment.created_at.strftime("%I:%M %p") %> • <%= comment.created_at.strftime("%d %b %y") %> <%= "~" + time_ago_in_words(comment.created_at) + " ago."%>
          </div>
        </div>
      </div>
    <% end %>

  <div class="mt-2" data-controller="reply">
    <% if policy(comment).create? %>
      <%= link_to "Reply", "#", class: "text-green-700", data: { action: "click->reply#show" } %> 
    <% end %>
    <% if policy(comment).destroy? %>
      <%= link_to "Delete", comment_path(comment, post_id: comment.commentable), method: :delete, class: "text-red-700", data: { confirm: "Are you sure?" } %>
    <% end %>
    <% if policy(comment).restore? %>
      <%= link_to "Undo", restore_comment_path(comment, post_id: comment.commentable), method: :patch, class: "text-red-700", data: { confirm: "Restore this comment?" } %>
    <% end %>
    <%= render partial: "comments/form", locals: {
      commentable: comment.commentable,
      parent_id: reply_to_comment_id(comment, comment.nesting),
      method: "post",
      class: "hidden",
      target: "reply.form"
    } %>
  </div>
  <%= tag.div id: "#{dom_id(comment)}_comments" do %>
    <%= render comment.comments, nesting: comment.nesting %>
  <% end %>
</div>

create.js.erb

<% if @comment.parent_id? %>
    var comments = document.querySelector("#<%= dom_id(@comment.parent) %>_comments")
<% else %>
    var comments = document.querySelector("#comments")
<% end %>

comments.insertAdjacentHTML('beforeend', '<%=j render partial: "comments/comment", locals: { comment: @comment, nesting: @comment.nesting }, format: :html %>')

var form = comments.parentElement.querySelector("form")
form.reset()

<% if @comment.parent_id? %>
  form.classList.add("hidden")
<% end %>

show.html.erb

  <h2 class="title-2">Comments</h2>

  <div class="mt-4">
    <div id="comments">
      <%= render partial: "comments/form", locals: {commentable: @post, method: "post" } %>
    </div>
  </div>

  <%= render @comments %>
</div>

20200901061626_add_nesting_to_comments.rb

class AddNestingToComments < ActiveRecord::Migration[6.0]
  def change
    add_column :comments, :nesting, :integer
  end
end

Posted in Deploy Ubuntu 18.04 Bionic Beaver Discussion

Hmmm tried this from a fresh rails 6 app.

cap production deploy is failing with

/home/userme/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute': Exception while executing as userme@123.456.789.10: rake exit status: 1 (SSHKit::Runner::ExecuteError) 
$HOME/.rbenv/bin/rbenv exec bundle exec rake assets:precompile
Compiling…
Compilation failed:
error Command "webpack" not found.

Webpacker docs is saying Make sure you have public/packs and node_modules in :linked_dirs for capistrano deployments. Then If you have node_modules added to :linked_dirs you'll need to run yarn install before deploy:assets:precompile and they have a snippet to add to your deploy.rb...

before "deploy:assets:precompile", "deploy:yarn_install"

namespace :deploy do
  desc "Run rake yarn install"
  task :yarn_install do
    on roles(:web) do
      within release_path do
        execute("cd #{release_path} && yarn install --silent --no-progress --no-audit --no-optional")
      end
    end
  end
end

Testing this now to see if it works... 10 mins later... nope... still busted... will try again later :/

I believe I may need to add capistrano-npm to my Gemfile so npm install can be run in the capistrano deploy scripts.

Posted in Test Driven Development Basics Discussion

I really like that you come at it fairly 'bare bones'... as in, no Rspec / FactoryBot / Fixtures / Cucumber / Faker / shoulda-matchers /guard etc etc. I recall when learning for the first time, it was just so much to take in for testinig. Your PORO approach makes it so simple and not as intimidating.

I did and it worked pretty well. I'll hunt down a sample of the code and share back here soon. Might take me a while though as I am away for a few days. I'll dump a bit below but I don't have time to cut out the sensitive info.

It was a little more to it than I first thought it would be. Here is a bit of stimulus. Notice the stuff with the Rails.Ajax that is querying my rails controller and returning json. So a bit is needed to be done in the controller to respond to that ajax request and only return the json in a format for stimulus to use.

I'm running it all from a new.html.erb that is requesting to create and then returning a show partial which allows the dropdown to be updated based on the selection of the previous dropdown. It really needs a demo of the whole thing I put together in a more generic way than what I had done in my application (ie do a simple country / state / city selector from Rails api all the way through to stimulus).

import { Controller } from "stimulus"

export default class extends Controller {

  static targets = [ "material", "price", "from", "query", "result", "button"]

  initialize() {
    console.log("Stimulus at your service!")
    this.updateQueryParams()
    this.toggleLoading()
  }

  get from () {
    return this.targets.find("from").value
  }

  get material() {
    return this.targets.find("material").value
  }

  get price() {
    return this.targets.find("price").value
  }

  toggleLoading() {
    this.targets.find("button").classList.toggle("is-loading")
  }

  updateToUnitOptions() {
    this.clearResult()

    Rails.ajax({
      type: "GET",
      url: "/alt_units.json",
      data: "material=" + this.material,
      success: (data) => {
        console.log('Alt Units were got!')
        this.refreshDropdownValues(data)
      }
    })
  }

  refreshDropdownValues(data) {
    let fromBefore = this.from
    this.fromTarget.innerHTML = ""
    for(var i = 0; i < data.length; i++) {
      var opt = data[i]
      this.fromTarget.innerHTML += "<option value=\"" + opt.name + "\">" + opt.name + "</option>"
    }
    this.fromTarget.value = fromBefore
    this.updateQueryParams()
  }

  clearResult() {
    this.queryTarget.innerHTML = ""
  }
}

Posted in Populate dropdowns based on selection with Stimulus JS

Wow. I was just deep diving in the console and wondering why this inside the callback was only referencing the Rails.ajax object... OK! Back on the road again!

I am now at the point where the dropdown menu 'toTarget' options need to be updated. Thanks for your help.

  updateToUnitOptions() {
    const material = this.material

    Rails.ajax({
      type: "GET",
      url: "/alt_units.json",
      data: "material=" + material,
      success: (data) => {
        console.log('Alt Units were got!')
        this.refreshDropdownValues(data)
      }
    })
  }

  refreshDropdownValues(data) {
    // update a Stimulus Target
    this.result = this.material
    this.toTarget <<<<< here is where I need to update selection option values.
    console.log(data)
  }
    ```

Posted in Populate dropdowns based on selection with Stimulus JS

Simply, all I am doing here is taking the material_id from a select dropdown, and then eventually I want to update the next dropdown menu item with the filtered list of alt_units that comes back from the alt_units_controller#index from rails.

Posted in Populate dropdowns based on selection with Stimulus JS

This is a follow up question from Populate dropdowns based on selection that I asked like... 3 years ago... and still haven't reallly done it very well.

Now that StimulusJS is here. Things seem more structured.

I have a stimulus controller below. After I complete the AJAX request, I want to send that data to another function just to keep my code clean. How do I call the this.doThingWithData(data) in the Ajax success callback? All I get so far is a TypeError: this.doThingWithData is not a function

import { Controller } from "stimulus"

export default class extends Controller {

  static targets = [ "material_id", "to_unit", "result"]

  get material_id() {
    return this.targets.find("material_id").value
  }

  updateToUnitOptions() {
    const material_id = this.material_id

    Rails.ajax({
      type: "GET",
      url: "/alt_units.json",
      data: "material_id=" + material_id,
      success: function(data) {
        message()
        this.doThingWithData(data)
      }
    })
    this.resultTarget.innerHTML = "You have selected material: " + material_id
  }

  doThingWithData(data) {
    // update a Stimulus Target
    console.log(data)
  }
}

function message() {
  console.log('Alt Units were got!');
}

Posted in rails link_to_if controller, action and format exist

I am trying to dynamically render some links to the index action on a range of controllers.

Essentially just create a table of all my models with a count of their records and a link_to the index page as html.

How would I link_to_if the controller, action and format combination exist? I have currently tried the below but because the route is the same but with different formats (csv/json). html doesn't exist so the link_to_if should fail and just render the model name.

resources_path  GET     /resourcess(.:format)    resourcess#index {:format=>/(csv|json)/}
<%= link_to_if klass.name, controller: klass.name.pluralize.underscore, action: 'index' if Rails.application.routes.url_helpers.method_defined?(klass.name.pluralize.underscore + '_path') %>
<% Dir[Rails.root.join('app/models/*.rb').to_s].each do |filename| %>
  <% klass = File.basename(filename, '.rb').camelize.constantize %>
  <% next unless klass.ancestors.include?(ActiveRecord::Base) %>
  <% next if klass.abstract_class? %>
    <% next if configuration_tables.exclude? klass.name %>
    <tr>
      <td><%= link_to_if klass.name, controller: klass.name.pluralize.underscore, action: 'index' if Rails.application.routes.url_helpers.method_defined?(klass.name.pluralize.underscore + '_path') %></td>
      <td><%= klass.count.to_s %></td>
  </tr>
<% end %>

Posted in Setup Windows 10 Discussion

@eliot I looked into this a few months ago and just accepted that it wasn't going to go away unless WSL was changed by Microsoft. Details at: https://github.com/Microsoft/WSL/issues/1426.

There appears to be ways to get rid of it but could introduce a security issue... I decided it was probably a bit of OCD on my behalf and to just let it go. I just find chasing these bugs means hours lost where I could actually be writing code. If you do figure out a good way please tell though.

Posted in Stimulus JS Framework Introduction Discussion

Hey Chris, FYI you have a spelling error in your code. 
  • Applicatoin instead of Application
  • stumulus instead of stimulus.
  • constrollers instead of controllers
  • $ needed in .js regex

import { Applicatoin } from 'stimulus'
import { autoload } from 'stumulus/webpack-helpers'

const application = Application.start()
const contsrollers = require.context("./controllers", true, /\.js$/ )

Even after I did this I had an error `TypeError: Object(...) is not a function`. I reviewed the stimulus installation guide and changed to:

import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("./controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

It looks like it should do the same thing. Funnily enough I tried 

import { Application } from "stimulus"
import { autoload } from "stimulus/webpack-helpers"

const application = Application.start()
const controllers = require.context("./controllers", true, /\.js$/)
application.load(autoload(controllers))

And it had the same error (all I did was rename 'context' to 'controllers' and 'definitionsFromContext' to 'autoload'

Maybe the name 'controllers' has become reserved?

Posted in Setup Windows 10 Discussion

Is there anything wrong with just running `sudo apt install rbenv` instead of the shell script? I was having issues with the script. Only the plugins folder was cloned...

Posted in Using Vagrant for Rails Development Discussion

Ended up chasing errors most of the day. Windows + Vagrant + Chef for ruby 2.4 and rails 5.1 was not worth the hassle for me as I will be the only dev on this project. Ended up using Vagrant then ssh'ing in and DIY server provision by following the usual GoRails guide -> https://gorails.com/setup/u...

Posted in Using Vagrant for Rails Development Discussion

I'm working through this now. https://github.com/applicat... Looks like this issue was closed long ago but has reemerged. Will try to come back with a fix if I figure it out... which I will probably forget to do once I have figured it out :P

Thanks Jacob,

I did have a read of this and have implemented it in my app for now. Would be nice to be able to get the ActiveRecord query builder to also use the SQL 'AS' alias method too so that it asks for:

SELECT [dbo].[dimCustomer].[CustomerCode] AS [id] FROM [dbo].[dimCustomer];

At the moment it will still send

SELECT [dbo].[dimCustomer].[CustomerCode] FROM [dbo].[dimCustomer];

And in BetterErrors if I inspect the Customer object it still refers to methods as the unaliased name.

But works for now so Thank You.

I am thinking it will be some kid of alias method on the model that emulates SQL AS

Essentially I am hoping to be able to use customer.id and rails knows I am meaning customer.CustomerCode or when ActiveRecord runs the SQL it passes something like

SELECT [dbo].[dimCustomer].[CustomerCode] AS [id] FROM [dbo].[dimCustomer];

Hi

I have a rails app that sits over an existing SQL Server database that I have no control over. Purely read only access to.

I have a model like Customer with fields like CustomerCode, CustomerName etc. Which means ugly unconventional Customer.first.CustomerCode and Customer.first.CustomerName.

Is there a way in the app/models/customer.rb to rename these columns so I can instead call Customer.first.id and Customer.first.name

Hopefully this is very straightforward.

Thanks

Posted in Free SSL with Rails and Nginx using Let's Encrypt

Cheers that is what I ended up doing. Had to figure out how to create the two server blocks but ended up with all as follows. This was two apps on one server, one app called example and the other called foo with foo directed to subdomain foo.example.com but using the example.com certificate.

/etc/nginx/sites-available/example.com
server {
listen 80;
listen [::]:80;

server_name example.com;
passenger_enabled on;
passenger_ruby /home/deploy/.rbenv/shims/ruby;
rails_env production;
root /home/deploy/example/current/public;

# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

listen 443 ssl;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_dhparam /home/deploy/dhparams.pem;
}

/etc/nginx/sites-available/foo.example.com
server {
listen 80;
listen [::]:80;

server_name foo.example.com;
passenger_enabled on;
passenger_ruby /home/deploy/.rbenv/shims/ruby;
rails_env production;
root /home/deploy/foo/current/public;

# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

listen 443 ssl;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_dhparam /home/deploy/dhparams.pem;
}

then symlinked them in the sites-enabled directory

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/foo.example.com /etc/nginx/sites-enabled/

then ran the lets encrypt using certbot

sudo certbot certonly --webroot --webroot-path /home/deploy/sales_playbook/current/public --renew-by-default --email me@jaykilleen.com --text --agree-tos -d example.com -d foo.example.com