Suggestions required to improve my first use of Rails Action Cable - ruby-on-rails

I have a task in my Rails app that can update a gallery album. This is passed an id that corresponds with an album on my Flickr account, it deletes all the photos attached to the album locally in the app and pulls them afresh from Flickr and updates the album name and title if required. This is required because I am currently working my way through all my photos re-tagging them all and this task will help pull those tags from Flickr to my site for searching and linking purposes.
I built an admin page that lists all my albums and each has an "Update Album" button. That looks like:
gallery.html.erb
<table class="striped">
<tr>
<th>Collections</th>
<th>Albums</th>
<th>Photo count</th>
<th>Flickr ID</th>
<th>Options</th>
</tr>
<% #collections.each do |c| %>
<tr>
<td><%= c.title %></td>
<td></td>
<td></td>
<td><%= c.flickr_id %></td>
<td></td>
</tr>
<% if c.has_albums? %>
<% c.albums.each do |a| %>
<tr class="<%= a.title.parameterize %>">
<td>↳</td>
<td>
<%= a.title %>
</td>
<td>
<%= a.photos.count %>
</td>
<td>
<%= a.flickr_id %>
</td>
<td class="this-one">
<%= link_to "Update",
admin_update_album_path(
album_flickr_id:a.flickr_id,
row: a.title.parameterize
),
"am-button": '',
method: :post,
remote: true
%>
</td>
</tr>
<% end %>
<% end %>
<% end %>
</table>
Because the task of updating took a while I thought putting it as a background job would be a good idea. Here's my controller and job:
gallery_controller.rb
class Admin::GalleryController < AdminController
def update_album
UpdateAlbumJob.perform_later(params[:album_flickr_id], params[:row])
end
end
update_album_job.rb
class UpdateAlbumJob < ApplicationJob
queue_as :default
after_perform do |job|
album = Album.find_by(flickr_id: job.arguments.first)
album.update_attribute(:updating, false)
puts job.arguments
ActionCable.server.broadcast 'album_updates_channel', content: 'Update complete', row: job.arguments[1], status: 'complete'
end
def perform(album_flickr_id, row)
require 'flickraw'
FlickRaw.api_key=ENV["FLICKR_API_KEY"]
FlickRaw.shared_secret=ENV["FLICKR_API_SECRET"]
flickr.access_token=ENV["FLICKR_OAUTH_TOKEN"]
flickr.access_secret=ENV["FLICKR_OAUTH_SECRET"]
# album_flickr_id = params[:album_flickr_id] or raise "No Album Flickr ID specified"
album = Album.find_by(flickr_id: album_flickr_id)
album.update_attribute(:updating, true)
ActionCable.server.broadcast 'album_updates_channel', content: 'Updating…', row: row, status: 'in-progress'
# Album.find_by(flickr_id: album_flickr_id).destroy if Album.find_by(flickr_id: album_flickr_id)
# Get tags
#flickr_tags_list = flickr.tags.getListUserRaw user_id: ENV["FLICKR_USER_ID"]
#tags_key = {}
#flickr_tags_list['tags'].each do |t|
#tags_key[t['clean'].to_sym] = t['raw'].first
end
# ========
# Work out parent collection
# This needs to cater for when there is NO parent collection
#collections = flickr.collections.getTree user_id: ENV["FLICKR_USER_ID"]
#parent_id = nil
def search_for_set_id_of_collection(set_id, collection, empty)
if collection["collection"]
collection.collection.each_with_index do |c|
search_for_set_id_of_collection(set_id, c, #parent_id)
end
end
if collection["set"]
collection["set"].each do |set|
if set["id"] == set_id
#parent_id = collection["id"]
end
end
end
end
#collections.each do |c|
search_for_set_id_of_collection(album_flickr_id, c, #parent_id)
end
# ========
Photo.where(album_id: album.id).destroy_all
Album.add_set_from_flickr(album_flickr_id, #parent_id, ENV["FLICKR_USER_ID"], #tags_key, true)
end
end
albumupdates.js:
const ActionCable = require('actioncable-modules');
const dotenv = require('dotenv')
dotenv.config()
const actionCable = ActionCable.createConsumer(process.env.WEBSOCKET_SERVER)
actionCable.subscriptions.create("AlbumUpdatesChannel", {
received: function(data) {
console.log('Hello from Action Cable');
if (data.status == 'in-progress') {
$('.' + data['row'] + ' td.this-one [am-Button]').hide();
$('.' + data['row'] + ' td.this-one').append(data['content']);
} else {
$('.' + data['row'] + ' td.this-one [am-Button]').show();
$('.' + data['row'] + ' td.this-one p').hide();
}
}
})
Basically, this is the first time i've just background jobs and action cable. I'm not 100% sure whether i've done this correctly but i wanted to give them a try and then ask on here for comments and suggestions. It sort of works ok but I have some questions:
Is this the best way of updating the table of albums with the status of the job? eg, "Updating..." and "Update complete!". I'd love to show an "Update Complete!" message but somehow remove that once it's been seen so just the button is showing again but unsure what path to go down to achieve that.
How would the live page updates work if i start an album updating, go away from the page and come back?
Also i'm unsure if my albumupdates.js file is set up correctly?
Just some comments and improvements to my code would be very helpful!
Thanks.

Related

Rails 7 paginate without sending API request

In my Rails 7 app I want to display transactions inside data table. because I expect there will be hundreds of them, I'm using pagy gem to paginate the result. Table data comes from client API request. As a response I get an array of hashes from which I extract data.
Here is my code:
Controller where I've made a request:
# transactions_controller.rb
class TransactionsController < ApplicationController
def index
response = client.transactions.list(platform_id: current_user.platform_id, page: 1, per_page: 100)
#transactions = response.body['data']
end
private
def client
#client ||= TestAPI::Client.new
end
end
Corresponding view:
#views/transactions/index.html.erb
<table class="table table-striped">
<thead>
<tr>
<b>
<tr>
<th>Date</th>
<th>Amount</th>
</tr>
</b>
</tr>
</thead>
<tbody>
<% #transactions.map do |transaction| %>
<tr>
<td>
<%= transaction['created_at'] %>
</td>
<td>
<%= transaction['amount_cents'] %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_bootstrap_nav(#pagy) %>
The pagination works well how to set it up without reloading the page? now when I use binding.pry in the controller to check if the request to the API was made, like below:
def index
response = client.transactions.list(platform_id: current_user.platform_id, page: 1, per_page: 100)
binding.pry
(...)
With each transition between pagination pages, a request is sent to the external API. How to send a request just once and operate on a single response?
[EDIT]
Controller with Low-Level Caching:
class TransactionsController < ApplicationController
def index
#pagy, #transactions = pagy_array(cashed_transactions.body['data'], items: 8)
end
private
def cashed_transactions
Rails.cache.fetch("merchants/#{current_user.platform_merchant_id}/transactions", expires_in: 12.hours) do
client.transactions.list(platform_merchant_id: current_user.platform_merchant_id, page: 1, per_page: 100)
end
end
end
Client API to get list of transactions:
def list(platform_merchant_id:, page: nil, per_page: nil, status: nil, platform_payment_id: nil)
filters = { 'filters[status]': status, 'filters[platform_payment_id]': platform_payment_id }.compact
params = { filters:, page:, per_page: }.compact
get("merchants/#{platform_merchant_id}/transactions", params:)
end

Problems in Rails undefined method `map' for #<Contact:0x000000013aa76b48>

I'm putting together a combo box or called the same as a select in rails, I put everything together but it gives me an error that tells me that I have a problem with the map inside the select, I'm using simple_form_for and I'm doing a map inside the collection inside the selector or called in simple_for associatio.
I copy the view and the controller
This view
<h1>HistContact#index</h1>
<p>Find me in app/views/hist_contact/index.html.erb</p>
<%= simple_form_for #histcontact, url:hist_contact_index_path do |f| %>
<% f.association :contact, collection: #contacts.map{|cont| [cont.name , cont.id]}%>
<%f.submit "buscar"%>
<% end %>
<table id = "client_table" class="table table-striped table-sm">
<thead>
<tr>
<th>Id</th>
<th>fecha</th
</tr>
</thead>
<tbody>
<% #histcontacts.each do |c|%>
<tr>
<td> <%= c.id %> </td>
<td> <%= c.created_at %></td>
</tr>
<% end %>
</tbody>
</table>
the controller
class HistContactController < ApplicationController
before_action :authenticate_user!
def index
#histcontacts = HistContact.all
#contacts = Contact.all
end
def new
#histcontact = HistContact.new
#contacts = Contact.new
end
def create
#histcontact = HistContact.find(contact_id: params[:contact])
end
private
def contactID(current_user)
client_id = Client.where(user_id: current_user.id)
contact_id = Contact.where(client_id: client_id.ids[0])
return contact_id
end
end
Thank you
According to error, you are trying to map a single object instead of an array of objects. Based on your controller code, the view file you shared is probably new.html.erb. To solve this problem you need do it like this:
def new
#histcontact = HistContact.new
#contacts = Contact.all
end

Submit multiple entries to database, from an array, in Rails on the same page

I'm trying to submit multiple entries into the database from an array. So, I create multiple new entries and store them in an array. In the view, I have a form of checkboxes for the user to check which ones he/she wants to add to the database. Upon clicking submit, I would like to add each of these forms to the database all in on shot. Here is the code:
Controller:
class EventsBoysController < ApplicationController
def new
#season = find_season
#meet = find_meet(#season)
#athletes_boys = current_coach.athletes.where(boy: true)
#events_boys = []
#athletes_boys.each do |athlete|
#events_boys << #meet.events_boys.new(:athlete_id => athlete.id)
end
#events = ["400 IH", "100", "1600", "400", "110 HH", "800", "3200", "200"]
end
def create
debugger
#season = find_season
#meet = find_meet(#season)
#events_boys = #meet.events_boys.create(events_boy_permit)
# debugger
if #events_boys.save
redirect_to #events_boys, notice: 'Season was successfully created.'
else
render action: "new"
end
end
private
def find_season
Season.find(params[:season_id])
end
def find_meet season
season.meets.find(params[:meet_id])
end
def events_boy_permit
params.require(:events_boy).permit(:season_id, :meet_id, :athlete_id, :boys_400_m_im, :boys_1600_m, :boys_400_m, :boys_110_m_hh, :boys_800_m, :boys_3200_m, :boys_200_m, :boys_100_m, :time_400_m_im, :time_1600_m, :time_400_m, :time_110_m_hh, :time_800_m, :time_3200_m, :time_200_m, :time_100_m, :place_400_m_im, :place_1600_m, :place_400_m, :place_110_m_hh, :place_800_m, :place_3200_m, :place_200_m, :place_100_m)
end
end
View:
<h1><%= "Create new events for the boys for #{#meet.name}" %></h1>
<table id="events-table">
<tr>
<th></th>
<% #events.each do |event| %>
<th><%= event %></th>
<% end %>
</tr>
<% #events_boys.each do |event| %>
<tr>
<td><%= Athlete.find_by_id(event.athlete_id).name %></td>
<%= form_for [#season, #meet, event], :html => { :mulitpart => true } do |f| %>
<td><%= f.check_box :boys_400_m_im %></td>
<td><%= f.check_box :boys_100_m %></td>
<td><%= f.check_box :boys_1600_m %></td>
<td><%= f.check_box :boys_400_m %></td>
<td><%= f.check_box :boys_110_m_hh %></td>
<td><%= f.check_box :boys_800_m %></td>
<td><%= f.check_box :boys_3200_m %></td>
<td><%= f.check_box :boys_200_m %></td>
<td><%= f.submit "Submit", :class => 'btn' %></td>
<% end %>
</tr>
<% end %>
</table>
<script type="text/javascript">
var athletes = new Array();
var arrayInAthletes = new Array();
element = document.getElementById("events-table");
var athletesArray = element.children[0].children
for (i=1; i < athletesArray.length; i++) {
var rowArray = athletesArray[i].children;
arrayInAthletes = [];
arrayInAthletes[0] = rowArray[0].innerText;
var sum = 0;
for (j=1; j < rowArray.length; j++) {
var checkBox = rowArray[j].children[0];
var checkedValue = $('#'+checkBox.id+':checked').val();
if (checkedValue === "1") {
arrayInAthletes[sum+1] = rowArray[j].children[0].id;
sum += 1;
}
}
athletes[i-1] = arrayInAthletes;
}
</script>
I think what you are looking for is something like the code below (needs to be adapted to your example).
Contact.create([
{first_name: 'Ryan', last_name: 'Smith', email: 'ryan#smith.com'},
{first_name: 'John', last_name: 'Doe', email: 'john#doe.com'}
])
All you need to do is put your data into an array of hashes. Each hash is an object you want to save. This will do a batch insert, which is what I think you want.
You will need to pull the values for checked/unchecked from your params since that is where they are passed to your controller. Cycle through that hash and perform your logic on that.

What is the recommended pattern for extend html class in view?

I have following <tr> tag in my table
<% if user.company.nil? %>
<tr class="error">
<% else %>
<tr>
<% end %>
<td><%= user.name %></td>
</tr>
I would like to add another if statement
<% if user.disabled? %>
<tr class="disabled">
<% end %>
So when two of this statements are true I would like to receive:
<tr class="error disabled">
I know I should move that to helper but how to write good case statment for extending class depends of this statements?
def tr_classes(user)
classes = []
classes << "error" if user.company.nil?
classes << "disabled" if user.disabled?
if classes.any?
" class=\"#{classes.join(" ")}\""
end
end
<tr<%= tr_classes(user) %>>
<td><%= user.name %></td>
</tr>
But the good style is:
def tr_classes(user)
classes = []
classes << "error" if user.company.nil?
classes << "disabled" if user.disabled?
if classes.any? # method return nil unless
classes.join(" ")
end
end
<%= content_tag :tr, :class => tr_classes(user) do -%> # if tr_classes.nil? blank <tr>
<td><%= user.name %></td>
<% end -%>
you could try a helper method, something like
def user_table_row(user)
css = ""
css = "#{css} error" if user.company.nil?
css = "#{css} disabled" if user.disabled?
content_tag :tr, class: css
end
not sure how well this will work in the case of a table row, as you will want to nest td inside it
UPDATE: here is updated version yielding the block of td code
def user_table_row(user)
css = # derive css, using string or array join style
options = {}
options[:class] = css if css.length > 0
content_tag :tr, options do
yield
end
end
then in the view
<%= user_table_row(user) do %>
<td><%= user.name %></td>
<% end %>

update_attributes is not working

Here's my controller
class ActivitiesController < ApplicationController
def exercises
if current_user.userprofile.present? #chef whether there is a userprofile object
#weeknum = current_user.userprofile.week
#dayly_activity = Activity.where(:week => 1, :day => 'Monday').first
end #end check userprofile
end
def updatexercises
respond_to do | format |
#dayly_activity = Activity.where(:week => 1, :day => 'Monday').first
#dayly_activity.update_attributes(params[:#dayly_activity])
#dayly_activity.save
format.html { render action: "exercises" }
end
end
end
And my template
<h1>WEEKLY EXERCICES</h1>
Day : <%= #dayly_activity.day %>
<%= form_for(#dayly_activity, :url => { :action => "updatexercises" }) do | f | %>
<table>
<tr>
<td>Jogging:</td>
<td>
<% list = (0..20).to_a %>
<%= f.select :jog, list %>
x 0.1 km
</td>
</tr>
<tr>
<td>Bicycling:</td>
<td>
<% list = (0..10).to_a %>
<%= f.select :bicycl, list %>
km
</td>
</tr>
<tr>
<td>Push ups:</td>
<td>
<% list = (0..20).to_a %>
<%= f.select :pushups, list %>
x 10 times
</td>
</tr>
<tr>
<td colspan = "2"><%= f.submit %></td>
</tr>
</table>
<% end %>
When I click the button, the Daily+activity object is not being saved. Am I missing some thing
EDIT
I've tried to hard code this way and it saving to the database.
#dayly_activity.jog = 17
#dayly_activity.pushups = 13
#dayly_activity.save
Obviously, the problem must be with the update_attributes
You need to use params[:dayly_activity] (drop the # sign).
Also, I would put these two lines :
#dayly_activity = Activity.where(:week => 1, :day => 'Monday').first
#dayly_activity.update_attributes(params[:dayly_activity])
Outside of your respond_to block (put them on top of it).
You can also drop the #dayly_activity.save, update_attributes do it automatically and will returns true/false if it works/fails.
You have error in [:#dayly_activity]
And in that code
#dayly_activity.update_attributes(params[:#dayly_activity])
#dayly_activity.save
save is useless. update_attributes saving the record.
It better to check result of update_attributes. So you can catch validation errors.
For example
if #dayly_activity.update_attributes(params[:dayly_activity])
redirect_to dayli_activity_path, :notice => "Updated"
else
render :edit
end

Resources