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
Related
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
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.
I am working in a Rails application and using the deferLoading: true option on jQuery DataTables in order pass the loading of the first DataTables to the Rails controller.
I have the datatable loading how I want it, loading the initial table in the controller gets rid of the Ajax delay when the initial html loads, however, the Datatable info section won't display the pagination results.
Code and images are shown below.
Again everything works except the pagination at the bottom of the table, I just cant get it to apply the same details as the Ajax calls to the datatable. Any ideas or direction on this issue would be greatly appreciated!
index.html.erb:
<div class="row">
<div class="col-xs-12 table-wrapper">
<div class="inner-wrapper">
<p class="quick-app">
<a class="custom-btn accent-inverse-btn add-user" href="<%= calculator_path%>">Quick Application</a>
</p>
<table class="table table-striped table-scroll cms-table-width dataTable" id="customer_deals_datatable" data-source="<%= dealer_customer_deals_url(:include_archived => params[:include_archived].present?) %>" >
<div>
<thead>
<tr>
<th>ID/Calculator</th>
<th>Applicant/Co-Applicant</th>
<th>Year</th>
<th>Model</th>
<th>App Status</th>
<th>Tier Number</th>
<th>Docs Status</th>
<th>Submitted On</th>
<th>Days Remaining</th>
<th>Chrome Decision</th>
<th>Updated At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% #datatable.data.each do |datum| %>
<tr>
<% datum[0] = datum[0].join('') %>
<%= (datum.map {|content| "<td>#{content}</td>"}.join('')).html_safe %>
</tr>
<% end %>
</tbody>
</div>
</table>
</div>
</div> <!-- </div>#content -->
</div>
controller
def index
respond_to do |format|
format.html do
params.merge!({"iDisplayLength"=>"10","iSortCol_0"=>"10","sSortDir_0"=>"desc"})
#datatable = CustomerDeals::CustomerDealsDataTable.new(view_context, #dealer)
end
format.json { render json: CustomerDeals::CustomerDealsDataTable.new(view_context, #dealer) }
end
end
here is a portion of the code from the datatable class in the project:
module CustomerDeals
class CustomerDealsDataTable
def fetch_deal_searches
return #deal_searches if #deal_searches.present?
deal_searches = CustomerDeals::CustomerDealSearch.where(dealership_id: #dealer )
if is_submitted_on_sort?
deal_searches = deal_searches.where('deal_dated_calculator_value != ?', 'calculator')
end
if params[:sSearch].present?
deal_searches = deal_searches.containing(params[:sSearch])
end
deal_searches = deal_searches.order(order_query)
#deal_searches = deal_searches
end
def is_submitted_on_sort?
SORT_COLUMNS[params[:iSortCol_0].to_i] == 'deal_submitted_on'
end
def lookup_sort_column
SORT_COLUMNS[params[:iSortCol_0].to_i]
end
def order_query
"#{lookup_sort_column} #{params[:sSortDir_0]}"
end
def paged_deal_searches
fetch_deal_searches.page(current_page_number).per(params[:iDisplayLength])
end
def current_page_number
params[:iDisplayLength].to_i == 0 ? 1 : params[:iDisplayStart].to_i/params[:iDisplayLength].to_i + 1
end
end
end
You're on the right track, deferLoading also can be assigned integer or array of two integers to specify how many records there are in the table for pagination to work.
From the manual:
deferLoading is used to indicate that deferred loading is required, but it is also used to tell DataTables how many records there are in the full table (allowing the information element and pagination to be displayed correctly).
In the case where a filtering is applied to the table on initial load, this can be indicated by giving the parameter as an array, where the first element is the number of records available after filtering and the second element is the number of records without filtering (allowing the table information element to be shown correctly).
Examples:
57 records available in the table, no filtering applied:
$('#example').dataTable( {
"serverSide": true,
"ajax": "scripts/server_processing.php",
"deferLoading": 57
} );
57 records after filtering, 100 without filtering (an initial filter applied):
$('#example').dataTable( {
"serverSide": true,
"ajax": "scripts/server_processing.php",
"deferLoading": [ 57, 100 ],
"search": {
"search": "my_filter"
}
} );
I think is a bug somewhere which is causing this problem for me since months already.
I am coding based on the Rails Cast #340 for using data tables with server side processing.
Below is the code I am using, one section, I have to more all with same error.
I know that was debated already 1 million times,I fetched all articles written on this topic, I tried almost everything but nothing worked for me to fix this.
From the main menu when clicking on the Table link I am getting:
DataTables warning (table id = 'allmeasurementstable'): DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.
The class code is below:
class MeasurementsDatatable
delegate :params, :h, :link_to, :number_to_currency, :number_with_precision, to: :#view
def initialize(view)
#view = view
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: Measurement.count,
iTotalDisplayRecords: records_m.total_entries,
aaData: data
}
end
private
def data
#records = records_m
#records.to_a.map do |record|
[
record.scheduled_reading_date,
record.inst_type,
link_to(record.inst_id, record), #record.inst_id,
record.scheduled_reading,
record.first_or_single_reading_taken,
record.first_or_single_reading_taken_date,
record.first_or_single_reading_not_taken_reason,
record.second_reading_taken,
record.second_reading_taken_date,
record.second_reading_not_taken_reason,
record.third_reading_taken,
record.third_reading_taken_date,
record.third_reading_not_taken_reason,
record.last_updated_by
]
end
end
def records_m
#records ||= fetch_records
end
def fetch_records
#looks like .all is not necessary like in the case of the list, see controller
if params[:sSearch].present?
#records = Measurement.order("#{sort_column}")
.where("
'%'||scheduled_reading_date||'%' LIKE :search
or upper(inst_type) LIKE upper(:search)
or (upper(inst_id) LIKE upper(:search)
or upper(scheduled_reading) LIKE upper(:search)
or upper(first_or_single_reading_taken) LIKE upper(:search)
or '%'||first_or_single_reading_taken_date||'%' LIKE :search
or upper(first_or_single_reading_not_taken_reason) LIKE upper(:search)
or upper(second_reading_taken) LIKE upper(:search)
or '%'||second_reading_taken_date||'%' LIKE :search
or upper(second_reading_not_taken_reason) LIKE upper(:search)
or upper(third_reading_taken) LIKE upper(:search)
or '%'||third_reading_taken_date||'%' LIKE :search
or upper(third_reading_not_taken_reason) LIKE upper(:search)
or upper(last_updated_by) LIKE upper(:search)
)", search: "%#{params[:sSearch]}%")
else
if params[:iSortCol_0].present?
#records = Measurement.all.order("#{sort_column}")
else
#records = Measurement.all
end
end
#records = #records.page(page).per_page(per_page)
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def sort_column
columns = %w[scheduled_reading_date inst_type inst_id scheduled_reading
first_or_single_reading_taken first_or_single_reading_taken_date first_or_single_reading_not_taken_reason
second_reading_taken second_reading_taken_date second_reading_not_taken_reason
third_reading_taken third_reading_taken_date third_reading_not_taken_reason
last_updated_by]
s = columns[params[:iSortCol_0].to_i] + " " + sort_direction(:sSortDir_0)
if params[:iSortCol_1].present?
s = s + "," + columns[params[:iSortCol_1].to_i] + " " + sort_direction(:sSortDir_1)
end
if params[:iSortCol_2].present?
s = s + "," + columns[params[:iSortCol_2].to_i] + " " + sort_direction(:sSortDir_2)
end
if params[:iSortCol_3].present?
s = s + "," + columns[params[:iSortCol_3].to_i] + " " + sort_direction(:sSortDir_3)
end
if params[:iSortCol_4].present?
s = s + "," + columns[params[:iSortCol_4].to_i] + " " + sort_direction(:sSortDir_4)
end
s
end
def sort_direction (n)
params[n] == "desc" ? "desc" : "asc"
end
end
The html code is here:
<% provide(:title, 'All Measurements') %>
<% if signed_in? %>
<h3>
All Measurements from: <%=#min_date%> to: <%=#max_date%> Records: <%=#count%>
</h3>
<p>
<%= link_to "List view", measurements_path, class: "btn btn-sm btn-primary" %>
</p>
<table id="allmeasurementstable" class="display table-bordered table-condensed table-responsive table-hover" data-source="<%= measurements_datatable_url(format: "json") %>">
<thead style="background-color: #bbbbbb;">
<tr>
<th>Scheduled Reading Date</th>
<th>Instrument Type</th>
<th>Instrument ID</th>
<th>Scheduled Readings</th>
<th>First or Single Reading Taken</th>
<th>First or Single Reading Taken Date</th>
<th>First or Single Reading Not Taken Reason</th>
<th>Second Reading Taken</th>
<th>Second Reading Taken Date</th>
<th>Second Reading Not Taken Reason</th>
<th>Third Reading Taken</th>
<th>Third Reading Taken Date</th>
<th>Third Reading Not Taken Reason</th>
<th>Last Updated by</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<% else %>
<%= render 'instruments/unsigned' %>
<% end %>
<script>
$(document).ready(function() {
$('#allmeasurementstable').dataTable({
bJQueryUI: true,
bDeferRender: true,
bStateSave: true,
bProcessing: true,
bServerSide: true,
sAjaxSource: $('#measurements_datatable_url').data('source')
}).fnSort( [ [0,'desc'],[2,'asc'] ] );
} );
</script>
The controller code is here (only the method for the table):
def allmeasurementstable
#min_date = Measurement.minimum(:scheduled_reading_date)
#max_date = Measurement.maximum(:scheduled_reading_date)
#count = Measurement.count
respond_to do |format|
format.html
format.json {
render json: MeasurementsDatatable.new(view_context)
}
end
end
Solved, moved to another version of Data Tables, last compiled and posted on data tables web site at the end of July 2014.
I am having some problems creating a loop that could show the cheapest price.
Here is my controller:
def domain
country_codes = ['.dk', '.com', '.eu', '.net', '.org', '.biz', '.info', '.nu', '.name', '.se', '.fi', '.net', '.de', '.it'] # etc. could move this to a config if needed
#domain = params[:domain]
#results = {}
country_codes.each do |cc|
#results[cc] = Whois.whois(#domain + cc)
end
#pricedk = Domain.sort("dk ASC").first
#pricecom = Domain.sort("com ASC").first
#priceorg = Domain.sort("org ASC").first
#pricenet = Domain.sort("net ASC").first
#ETC...
end
My view:
<table border="0" bordercolor="#FFCC00" width="700" cellpadding="0" cellspacing="0">
<tr class="top">
<td class="checkdomain"></td>
<td>Name</td>
<td>Domain</td>
<td style="font-size:9px;"></td>
</tr>
<% #results.each_pair do |country_code, available| %>
<% klass = available.registered? ? "pinfo" : "info" %>
<tr>
<td><span class="<%= klass %>"></span></td>
<td><%= #domain + country_code %></td>
<td>PRICE HERE</td>
</tr>
<% end %>
</table>
I want to DRY the #pricecom, #pricedk, #priceorg up.
How do I include it in the #results loop?
When you have repetition like this, with many different instance variables, what you need to do is roll them all up into a single instance variable that's a Hash:
def domain
country_codes = %w[ dk com eu net org biz info nu name se fi net de it ]
#domain = params[:domain]
#results = { }
#prices = { }
country_codes.each do |cc|
#results[cc] = Whois.whois("#{#domain}.#{cc}")
#prices[cc.to_sym] = Domain.sort(cc).first
end
end
You'll see this pattern every so often. #pricesdk becomes #prices[:dk]