I'm using Rails 7 with gem 'roo' 2.9 for doing an excel import. Everything went fine so far.
Now i want to delete part of formerly imported data from my database (postgresql) before i repeat the import with the same excel file.
I have no unique attribute or attribute set in my dataset to decide which data to delete.
Example: My Excelfile contains the following attributes:
employee_name
costcenter
working_hours
I have an separate excelfile for every year. So when i repeat the import of an explicit file i want to delete only the data for the corresponding year. But the attribute "year" is not included in the importfile.
My idea is, to
add the attribute "year" to my model,
submit it from my gui during import and
save it to every dataset.
Topic 1 and 2 are working, but i have no idea, how to save it to the database. I am always getting a NIL value for the year.
This is my code:
model
class OvertimesImport
include ActiveModel::Model
require 'roo'
attr_accessor :file
attr_accessor :year
def initialize(attributes={})
attributes.each { |name, value| send("#{name}=", value) }
end
def persisted?
false
end
def open_spreadsheet
case File.extname(file.original_filename)
when ".csv" then Csv.new(file.path, nil, :ignore)
when ".xls" then Roo::Excel.new(file.path, nil, :ignore)
when ".xlsx" then Roo::Excelx.new(file.path)
else raise "Unknown file type: #{file.original_filename}"
end
end
def load_imported_overtimes
spreadsheet = open_spreadsheet
header = spreadsheet.row(1)
(2..spreadsheet.last_row).map do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
overtime = Overtime.new
overtime.attributes = row.to_hash
overtime
end
end
def imported_overtimes
#imported_overtimes ||= load_imported_overtimes
end
def save
if imported_overtimes.map(&:valid?).all?
imported_overtimes.each(&:save!)
true
else
imported_overtimes.each_with_index do |overtime, index|
overtime.errors.full_messages.each do |msg|
errors.add :base, "Row #{index + 2}: #{msg}"
end
end
false
end
end
end
controller
class OvertimesImportsController < ApplicationController
def new
#overtimes_import = OvertimesImport.new
end
def create
delete_old_overtimes
#overtimes_import = OvertimesImport.new(params[:overtimes_import])
respond_to do |format|
if #overtimes_import.save
format.html { redirect_to root_path, notice: "Overtimes successfully imported." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
private
#Delete all former import data
#Here i want to delete only data from a special year
def delete_old_overtimes
Overtime.where(year: ???????).delete_all
end
end
view
<%= form_for #overtimes_import do |f| %>
<% if #overtimes_import.errors.any? %>
<%= pluralize(#overtimes_import.errors.count, "error") %> prevented us from
importing your spreadsheet. Please update your spreadsheet and try agin.
<ul>
<% #overtimes_import.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<div>
<div class="row mt-4">
<div class="col-4">
<%= f.label :year, class: "form-label" %>
<%= f.text_field :year %>
</div>
</div>
<div class="row mt-4">
<div class="col-4">
<%= f.file_field :file %>
</div>
</div>
<div class="row mt-4">
<div class="col-2">
<%= f.submit "Import File", class: 'btn btn-outline-success' %>
</div>
</div>
</div>
<% end %>
How and where to put the code to submit the attribute "year" to my database and use it to delete the corresponding data before the import?
I think you must save the year to the database instead of using it like an attribute accessor.
Related
I have a Rails app that searches through a DB and returns items. At the moment in my view it automatically returns the default search results ("") without me needing to hit my submit_tag. How do I go about only making this action happen once I have hit the submit_tag? Any help would be greatly appreciated!
Here is my view:
<%= form_tag(new_design_path, method: :get) do %>
<%= label_tag(:q, "Search all designs:") %>
<%= text_field_tag(:q) %>
<%= submit_tag("Search") %>
<% end %><br>
<!-- Button to return one random design -->
<%= form_tag(new_design_path, method: :get) do %>
<%= label_tag(:q, "Inspire me! Click here for a random design:") %>
<%= submit_tag("Random Design") %>
<% end %>
<h2>Search results:</h2>
<% #random.each do |design| %>
<h3><%= design['name'] %></h3>
<h5><%= image_tag design['thumbnail_url'] %></h5>
<% end %>
<% #search.each do |design| %>
<div class="design">
<h3 class="design_name"><%= design['name'] %></h3>
<h5><%= image_tag design['thumbnail_url'] %></h5>
<%= button_to 'Save to Favourites',
designs_path(
design: design.slice('name', 'thumbnail_url')
),method: :post %>
</div>
<% end %>
And my controller:
class DesignsController < ApplicationController
def index
#designs = Design.all.order("created_at DESC")
end
def new
# returns an array of hashes
#search = SpoonflowerApi.new.find(params[:q])['results']
#random = SpoonflowerApi.new.random(rand(1..740579), 1)['results']
end
def create
#design = Design.new(design_params)
#design.save
if #design.save
flash[:notice] = 'Design has been added to favourites!'
else
flash[:notice] = 'Design already in Favourites!'
end
redirect_to new_design_path
end
def destroy
#design = Design.find(params[:id])
#design.destroy
flash[:notice] = 'Design removed from favourites!'
redirect_to designs_path
end
private
def design_params
params.require(:design).permit(:name, :thumbnail_url)
end
end
new is used to populate the initial form, so if you don't want anything for those fields you should just set both #search and #random to an empty array in new. You don't show any code for your model, so it's not really clear what Api is.
show should be called once you submit the form
def new
#search = []
#random = []
end
then move the logic to provide the search results or random record into the show method
def show
# not sure what you want to do here
# since it seems like you have 2 buttons you need logic to provide data
# based on the button
# maybe something like this
if params[:q].nil?
#search = []
#random = Api.new.random(rand(1..740579), 1)['results']
else
#search = Api.new.find(params[:q])['results']
#random = []
end
end
If I understood you correctly, your view showing some search results before you click the search button.
Since you directly send the params[:q] to SpoonflowerApi, I am guessing that it returns some default value and your view draw it.
Simply update your controller to:
def new
#search=[]
#search = SpoonflowerApi.new.find(params[:q])['results'] unless params[:q].nil?
#random = SpoonflowerApi.new.random(rand(1..740579), 1)['results']
end
I am trying to show all my items upon loading the page, and if the date is selected (using materializecss datepicker), to show only the date range of items. Showing all the items is easy as I just need to do #all_requests = Request.all, but I have spent two nights trying various methods like form_for and form_tag etc. Currently with the code below it shows only today's items because of the default Date.today, but if i remove the default it gives a nil error, I think because the date has not been selected yet...
Index.html.erb
<%= form_tag requests_path, method: :get do %>
<%= text_field_tag 'search[date_from]', #search.date_from, placeholder: 'From date', class: 'datepicker' %>
<%= text_field_tag 'search[date_to]', #search.date_to, placeholder: 'To date', class: 'datepicker' %>
<%= submit_tag 'Search', class: 'btn waves-effect waves-light formbttn' %>
<% end %>
<div class="row">
<% #all_requests.each do |request| %>
...
Request.search.rb
class RequestSearch
attr_reader :date_from, :date_to
def initialize(params)
params ||= {}
date_from = parsed_date(params[:date_from], Date.today.to_s)
date_to = parsed_date(params[:date_to], Date.today.to_s)
end
def scope
Request.where(:date => 'date_from'..'date_to')
end
def parsed_date(date_string, default)
Date.parse(date_string)
rescue ArgumentError, TypeError
default
end
end
Requests_controller.rb
def index
# if params[:search] == nil
# #all_requests = Request.all
# else
#search = RequestSearch.new(params[:search])
#all_requests = #search.scope
end
Materializecss DOM
<input type="text" name="" id="search_date_from" value="2017-08-13"
placeholder="From date" class="datepicker picker__input" readonly=""
tabindex="-1" aria-haspopup="true" aria-expanded="false"
aria-readonly="false" aria-owns="search_date_from_root">
class RequestSearch
attr_reader :date_from, :date_to
# use keyword args instead
def initialize(date_from: nil, date_to: nil, **kwargs)
# You need to use `#` to set instance vars
#date_from = parsed_date(date_from)
#date_to = parsed_date(date_to)
end
def scope
Request.where(date: date_from..date_to)
end
# reduce the arity by setting the default in the method
# definition
def parsed_date(date_string, default = Date.today.to_s)
Date.parse(date_string)
rescue ArgumentError, TypeError
default
end
end
I have two models:
class Location < ActiveRecord::Base
has_many :cows
accepts_nested_attributes_for :cows
end
class Cow < ActiveRecord::Base
belongs_to :location
end
Every location has an id, a name and three boolean values. A cow consists of an id, two strings and two dates and it also includes a location_id.
In my cow view I have a dropdown of every location. It gets updated automatically whenever I create, edit or delete a location.
Below the dropdown you can see all cows ever created. Afer that there is a form to add a new cow with the two string fields and two date fields.
Now I want two things: Whenever I select a value in the dropdown, the displayed cows should change. If I select for example location 2, only cows that have a location_id = 2 in the table should be shown. Also when I create a new cow, the id of the selected location should be saved as well (in this example location_id = 2 should be saved in the cow row when I click the button at the very bottom).
This is how the dynamic dropdown looks like in the cow index.html file:
<%= simple_form_for Cow.new do |f| %>
<%= f.collection_select :location, Location.order('name').all, :id, :name, { prompt: "Ort auswählen" } %>
<% end %>
And this is my cow controller:
def index
if (params[:location] && Location.all.collect(&:name).include?(params[:location][:name]))
#cows = Cow.send(params[:location][:name].downcase)
else
#cows = Cow.all
end
end
# (...)
def create
#cow = Cow.new(cow_params)
# (...)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_cow
#cow = Cow.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def cow_params
params.require(:cow).permit(:ohrmarke, :hin, :weg, :stallnummer, :location_attributes => [:id])
end
end
The edited index part just does not work. Perhaps because I have a dropdown for Cow.new and I don't have a submit button. Now the thing is: I need the dropdown (value) for a new cow but also want to display cows that fit to the selected dropdown value. Also I don't want to use a submit button just for the dropdown box. Is there any solution for my problem? I googled so much but just can't find the right answer for my problem.
Edit:
I edited my cows controller and my index page:
cow controller:
def index
#locations_all = Location.all
if (params[:location] && cows = Cow.where(location_id: params[:location]))
#cows = cows
#location = Location.where(id: params[:location])
else
#cows = Cow.all
end
end
# (...)
def create
#cow = Cow.new(cow_params)
# (...)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_cow
#cow = Cow.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def cow_params
params.require(:cow).permit(:ohrmarke, :hin, :weg, :stallnummer, :location_attributes => [:id])
end
cow index.html:
<%= simple_form_for Cow.new do |f| %>
<%= f.collection_select :location, Location.order('name').all, :id, :name, { prompt: "Ort auswählen" } %>
<ul>
<% #locations_all.each do |loc| %>
<li><%= link_to loc.name, #cows.index(location: loc.id) %></li>
<% end %>
</ul>
<%= f.hidden_field :location_id, value: #location_id %>
<table class="striped">
<thead class="thead-default">
<tr>
<th>Ohrmarke</th>
<th>Stallnr.</th>
<th>Hin/Weg</th>
</tr>
</thead>
<tbody>
<!-- ... -->
</tbody>
</table><br>
<%= f.input :ohrmarke, as: :string, input_html: { maxlength: 5 } %>
<%= f.input :stallnummer, as: :string, input_html: { maxlength: 3 } %>
<!-- datepicker not functional yet -->
<input type="date" class="datepicker" id="hin" placeholder="hin">
<input type="date" class="datepicker" id="hin" placeholder="weg">
<button class="btn waves-effect waves-light" type="submit" name="action">Zuordnung speichern
<i class="material-icons right">send</i>
</button>
<% end %>
Drop-down & Content
This is a possible implementation for cows_controller#index.
def index
#locations_all = Location.all
if (params[:location] && cows = Cow.where(location_id: params[:location]))
#cows = cows
#location = Location.where(id: params[:location])
else
#cows = Cow.all
end
end
There are however a few important points to be made and a few problems.
The code you're using for the drop-down assumes you're making a POST request which isn't necessary in this case. The same can be achieved with a simple link_to. In order for this to work, you'd have something similar to this:
<ul>
<% #locations_all.each do |loc| %>
<li><%= link_to loc.name, cows_path(location: loc.id) %></li>
<% end %>
</ul>
Form
In order for the form to create a new Cow using the correct location you'd need to include a hidden_field inside it.
<%= f.hidden_field :location_id, value: #location.id %>
But this introduces a design problem. What happens when you haven't selected any location on the drop-down menu? There are plenty of ways to handle this problem, but my favorite solution is to hide the form in that case.
<% unless #location %>
#form-block ...
<% end %>
This solutions seems ideal if cows can't be created without a specified location.
Edit
Ok, now that I can see you HTML. I can tell you what wrong. Your simple_form_for block is surrounding the whole view. Which is probably not what you want. forms are usually POST requests, so you only need those when you plan to send data, or create a new entry on a table. link_to is a GET request which you can request information to the server. This a small example of how my view would look like:
#drop-down menu
<div id="cow_menu">
<ul>
<% #locations_all.each do |loc| %>
<li><%= link_to loc.name, cows_path(location: loc.id) %></li>
<% end %>
</ul>
</div>
#table
<table class="striped">
# your table logic...
</table>
#new Cow form
<%= simple_form_for Cow.new do |f| %>
<%= f.hidden_field :location_id, value: #location_id %>
<%= f.input :ohrmarke, as: :string, input_html: { maxlength: 5 } %>
<%= f.input :stallnummer, as: :string, input_html: { maxlength: 3 } %>
<%= f.submit "Zuordnung speichern", class="btn waves-effect waves-light"%>
<% end %>
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 7 years ago.
Improve this question
I'm trying to show a message on the new view
Here the live demo:
http://code.runnable.com/VrUvfZPOiiVooTAU/importing-excel-files-for-ruby-on-rails
Here is the controller:
def new
end
def create
#errors = User.import(params[:file])
if #errors.present?
render :new;
return;
end
end
Here is the view
Import
<%= form_tag user_management_users_path, multipart: true do %>
<%= file_field_tag :file %>
<%= submit_tag "Import" %>
<% end %>
<% if #errors %>
<% #errors.each do |message| %>
<%= message %>
<% end %>
<% end %>
Here is the model:
def self.import(file)
#errors = []
spreadsheet = open_spreadsheet(file)
(2..spreadsheet.last_row).each do |i|
name = spreadsheet.cell(i,'A')
lastname = spreadsheet.cell(i,'B')
doc_nat = spreadsheet.cell(i,'C')
user = User.create(:name => name,
:lastname =>lastname,
:doc_nat =>doc_nat)
if user.save
# stuff to do on successful save
else
user.errors.full_messages.each do |message|
#errors << "Issue line #{i}, column #{message}"
end
end
end
end
def self.open_spreadsheet(file)
case File.extname(file.original_filename)
when ".csv" then Roo::CSV.new(file.path, csv_options: {encoding: "iso-8859-1:utf-8"})
when ".xls" then Roo::Excel.new(file.path, packed: false, file_warning: :ignore)
when ".xlsx" then Roo::Excelx.new(file.path, nil, :ignore)
else raise "Unknown file type: #{file.original_filename}"
end
end
I'm trying to find the way to show a message indicating the ROW PROBLEM on the index view.
For example I'm adding a user that already exist and want to show a message indicating
ROW 1 already exist
Row 2 is duplicated
Row 1 must be string
or something like that
But after uploading the excel just getting numbers and not the message:
Issue line 2, column already exist
Issue line 3, column already exist
Just getting numbers:
2 3
If the user can't be saved that means it failed validation (although the only validation you have at the moment is on doc_nat... you may want to add validations for the "must be string" conditions, etc).
For an active_record object that failed validation, there's an array object.errors.full_messages which is all the errors found when validating.
So...
def self.import(file)
#errors = []
spreadsheet = open_spreadsheet(file)
(2..spreadsheet.last_row).each do |i|
name = spreadsheet.cell(i,'A')
lastname = spreadsheet.cell(i,'B')
doc_nat = spreadsheet.cell(i,'C')
user = User.new(:name => name,
:lastname =>lastname,
:doc_nat =>doc_nat)
if user.save
# stuff to do on successful save
else
user.errors.full_messages.each do |message|
#errors << "La información de la línea #{i}, columna #{message}"
end
end
end
#errors # <- need to return the #errors array
end
and in the view...
<% if #errors %>
<% #errors.each do |message| %>
<%= message %>
<% end %>
<% end %>
I'm making a form that creates more than one record for the user depending on how many items the user decides to check off in the form using checkboxes.
Currently, I'm running into an error where param is missing or the value is empty: itemrecord even though in the log, it appears that params are passing through:
{"utf8"=>"✓", "authenticity_token"=>"m2NMruoFRr6lpsuVMK9UthlY0bsJsPmf1LWce2uKaH4=", ":item_name"=>["Backpack", "Water filter"], "commit"=>"Go!"}
Model relationship is that a User has_many :inventories
Controller code:
def create
#itemrecord = #current_user.inventories.build
items_to_be_saved = []
inventory_params.each do |i|
items_to_be_saved << ({ :signup_id => #current_user.id, :item_name => i })
end
if Inventory.create items_to_be_saved
flash[:success] = "Thanks!"
redirect_to root_path
else
render new_inventory_path
end
end
def inventory_params
params.require(:itemrecord).permit(:item_name)
end
View code:
<%= form_for #itemrecord do |f| %>
<!-- In case you're wondering, the #wishlist below is basically a hash of categories of items and items. This hash is updated in the controller, and then used by multiple views to create the same table of items. -->
<% #wishlist.each do |category, list| %>
<div class="col-xs-2">
<div class="form-group box">
<h5> <%="#{category}"%> </h5>
<% list.each do |thing| %>
<%= check_box_tag ":item_name[]", "#{thing}" %>
</br>
<% end %>
</div>
</div>
<% end %>
<%= f.submit "Go!", class: "btn btn-primary btn-large btn-block" %>
</div>
<% end %>
By the way I also tried changing :item_name to :item_names to account for the array based on what else I read on SO, but that didn't fix it either
Take a look at your inventory_params function. You're saying that you require an itemrecord, and permit an item_name attribute. Observe:
def inventory_params
params.require(:itemrecord).permit(:item_name)
end
However, in the parameters being passed, there is no reference to an itemrecord object whatsoever, but there is a reference to item_name. A quick change to your inventory_params method, removing the :itemrecord requirement and instead requiring :item_name, will fix your issue.
def inventory_params
params.require(:item_name)
end
While this isn't necessarily the best way to go about doing this (I'd suggest reading up on your Active Record Form Helpers), it should solve your issue.