I have this line of code:
<%= f.select(:state, :id, Location.all.collect { |l| [ l.location, l.id] }, {:class => "form-select"}) %>
but it keeps throwing a: no implicit conversion of Array into Hash
Let’s say you want to be able to select the state and it's id as a part of your params hash.
Rails provides select_tag helper in conjunction with the options_for_select helper so you can create these in your view by just iterating over collection.
options_for_select expects a very specific input – an array of arrays which provide the text for the dropdown option and the value it represents. This is great, because that’s exactly what your part of code does Location.all.collect { |l| [ l.location, l.id] }
Example:
<%= f.select(:state, options_for_select(Location.all.collect { |l| [ l.location, l.id] }),{:class => "form-select"}) %>
So, options_for_select(Location.all.collect { |l| [ l.location, l.id] }) will create a couple of option tags, one for each choice, like this: options_for_select([["choice1",1],["choice2",2]])
Also, if you want to avoid the options_for_select option, you can you use generic select helper.
Related
I'm building a Rails 5+ API with JSON as the format between front and back ends.
I want the ability to create a single record or multiple records, depending if an array of JSON objects is sent.
Note that I'm NOT using JSON:API spec, but rather the JSON objects are coming as root attributes.
# Create a single child object for the associated parent
POST api/v1/parents/1/children PARAMS: { child_name: "Alpha" }
# Create multiple children objects for the associated parent
POST api/v1/parents/1/children PARAMS: [{ child_name: "Alpha" }, { child_name: "Bravo" }]
In the controller I have to differentiate if a single object or an array is being sent. It seems that Rails converts JSON data to a params["_json"] key automatically if the Content-Type="application/json" header is set, and I'm using this to tell if an array was passed or not.
class ChildrenController < ApplicationController
def create
#parent = Parent.find(params[:parent_id])
if params["_json"] && params["_json"].is_a?(Array)
#children = []
params["_json"].each do |child_attributes|
#children << #parent.children.create!(child_attributes)
end
render json: #children
else
#child = #parent.children.create!(child_params)
render json: #child
end
end
def child_params
params.permit(:child_name)
end
end
Questions
Is using params["_json"] a standard way to tell if an array was passed or not? It seems hacky, but I'm not sure of a better way.
If an array of JSON objects is passed, how can I still use the StrongParameters child_params method? Currently, if the user passes an array of JSON objects, they can put whatever attributes they want and I'm not filtering them out.
Is there a better way to implement this functionality? I don't have to use a single endpoint for both single and multiple creation, I just thought it would be more convenient to have a single API endpoint that can handle single or multiple objects.
I also plan on creating a single endpoint for the update action that can also accept single or multiple objects. Is this a bad practice?
In the case you send a JSON array (like [{ child_name: "Alpha" }, { child_name: "Bravo" }]) to an endpoint, Rails stores it into _json of the params hash. The reason for doing this is, that unlike a single JSON object, an array cannot be extracted into a hash, without introducing another key.
Let's say, that we post { child_name: "Alpha" } to an endpoint. The params will look like:
{"child_name"=>"Alpha", "format"=>:json, "controller"=>"api/children", "action"=>"create"}
Now if we post the array [{ child_name: "Alpha" }, { child_name: "Bravo" }] and it would be blindly extracted into the hash like a JSON object the result would look like:
{[{ child_name: "Alpha" }, { child_name: "Bravo" }], "format"=>:json, "controller"=>"api/children", "action"=>"create"}
This is not a valid hash! So Rails wraps your JSON array into _json and it becomes:
{"_json" => [{ child_name: "Alpha" }, { child_name: "Bravo" }], "format"=>:json, "controller"=>"api/children", "action"=>"create"}
This happens here in the actionpack. Yes, it seems a bit hacky, but the only and maybe cleaner solution would be to wrap all data from the request body into one key like data.
So to your actual question: How can I manage a JSON object and JSON array at a single endpoint?
You can use strong params like ..
def children_params
model_attributes = [:child_name]
if params.key? '_json'
params.permit(_json: model_attributes)['_json']
else
params.permit(*model_attributes)
end
end
.. and in the create action would look like ..
def create
result = Children.create!(children_params)
if children_params.kind_of? Array
#children = result
render :index, status: :created
else
#child = result
render :show, status: :created
end
end
Of course you would need to adapt it a bit for your specific use case. From the design perspective of your API, I think it's okay doing it like that. Maybe this should be asked within a separate question.
I've been seeing topics dancing around this issue, so maybe I've actually seen the answer and I've just been banging away at it too long that my brain can't recognize it.
In short, I think I need to mark my form data as dirty so that it will update the array field in the Freelancer object, but I seem to be missing something.
Important info: programmingLanguages below is an array field. In my migration I have:
t.text :programmingLanguages, array: true, default: []
So while I'm currently submitting only one string to said array, I want to be able to submit more than one eventually (and perform operations on the data when I need to down the line...)
I have a form:
<%= form_for(#freelancer) do |f| %>
<%= f.label :programmingLanguages, "Programming languages (seperated by commas): "%>
<%= f.text_field :programmingLanguages %>
.
.
.
<%= f.submit "Submit", class: "button"%>
<% end %>
Which submits fine and gives me a list of params I expect:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"long token", "freelancer"=>{"programmingLanguages"=>"Stuff for an array, more stuff for an array"...}
I then use those params to update attributes for my #freelancer object:
def create
#freelancer = Freelancer.find(session[:user_id])
if #freelancer.update_attributes(freelancer_params)
redirect_to #freelancer
else
render root_path
end
end
Because I'm using a multi-step form (hence the find by session), I'm updating the attributes in my create action vs. saving them because the object I'm working with already exists and is saved to the database. The issue is that when I use update_attributes I notice that the SQL commits are ignoring my array fields, such as programmingLanguages above, but does not disregard the other attributes in the params. In other words my integer field totalPrograms updates easily/correctly, but my 4 array fields get lost somewhere between the params and updating the #freelancer object.
Any help much appreciated!
Update:
Here are the strong params:
def freelancer_params
params.require(:freelancer).permit(:programmingLanguages, :totalPrograms...)
end
One more note with this, I guess: I've tried indicating here that :programmingLanguages is an array (a la :programmingLanguages => [] or programmingLanguages: []) and I get syntax errors (syntax error, unexpected ',', expecting =>). Which of course could just mean my syntax is off in setting theses as arrays in my strong params, but if that's the case then I can't find the dumb little error there either.
#!/usr/bin/ruby
# Fetches all Virginia Tech classes from the timetable and spits them out into a nice JSON object
# Can be run with option of which file to save output to or will save to classes.json by default
require 'rubygems'
require 'mechanize'
require 'nokogiri'
require 'json'
#Create Mechanize Browser and Class Data hash to load data into
agent = Mechanize.new
classData = Hash.new
#Get Subjects from Timetable page
page = agent.get("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
subjects = page.forms.first.field_with(:name => 'subj_code').options
#Loop subjects
subjects.each do |subject|
#Get the Timetable Request page & Form
timetableSearch = agent.get("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
searchDetails = page.forms.first
#Submit with specific subject
searchDetails.set_fields({
:SUBJ_CODE => subject,
:TERMYEAR => '201401',
:CAMPUS => 0
})
#Submit the form and store results into courseListings
courseListings = Nokogiri::HTML(
searchDetails.submit(searchDetails.buttons[0]).body
)
#Create Array in Hash to store all classes for subjects
classData[subject] = []
#For every Class
courseListings.css('table.dataentrytable/tr').collect do |course|
subjectClassesDetails = Hash.new
#Map Table Cells for each course to appropriate values
[
[ :crn, 'td[1]/p/a/b/text()'],
[ :course, 'td[2]/font/text()'],
[ :title, 'td[3]/text()'],
[ :type, 'td[4]/p/text()'],
[ :hrs, 'td[5]/p/text()'],
[ :seats, 'td[6]/text()'],
[ :instructor, 'td[7]/text()'],
[ :days, 'td[8]/text()'],
[ :begin, 'td[9]/text()'],
[ :end, 'td[10]/text()'],
[ :location, 'td[11]/text()'],
# [ :exam, 'td[12]/text()']
].collect do |name, xpath|
#Not an additional time session (2nd row)
if (course.at_xpath('td[1]/p/a/b/text()').to_s.strip.length > 2)
subjectClassesDetails[name] = course.at_xpath(xpath).to_s.strip
end
end
#Add class to Array for Subject!
classData[subject].push(subjectClassesDetails)
end
end
#Write Data to JSON file
open(ARGV[0] || "classes.json", 'w') do |file|
file.print JSON.pretty_generate(classData)
end
The above code is supposed to retrieve data from https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest
but if I print subjects.length is prints 0 so it clearly isn't getting the correct data. The given term code "201401" is definitely the right one.
I've noticed that when I manually enter in the link to my browser the subject field doesn't allow you to select an option until a term is selected, however when I view the page source the data is clearly already there. What can I do to retrieve this data?
I'm looking at that vtech page and I can see that you need to select a TERMYEAR first before the subj_code dropdown fills allowing you to get the options. Unfortunately this happens with javascript in function dropdownlist(listindex). Mechanize doesn't handle javascript so this script is doomed to fail.
Your options are to run a browser automator like Watir or Selenium: discussed here: How do I use Mechanize to process JavaScript?
Or to read the source of that page and parse out the values of these lines:
document.ttform.subj_code.options[0]=new Option("All Subjects","%",false, false);
document.ttform.subj_code.options[1]=new Option("AAEC - Agricultural and Applied Economics","AAEC",false, false);
document.ttform.subj_code.options[2]=new Option("ACIS - Accounting and Information Systems","ACIS",false, false);
To get the options. You could do that by simply using open-uri:
require 'open-uri'
page = open("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
page_source = page.read
Now you can use a regex to scan for all the options:
page_source.scan /document\.ttform.+;/
That'll give you an array with all the lines that have the javascript codes that contain the options. Craft your regex a little better and you can extract the option text from those. I'll see if I can come up with something for that and I'll post back. Hopefully this will get you headed in the right direction.
I'm back. I was able to parse out all the subj_code options with this regex:
subjects = page_source.scan(/Option\("(.*?)"/).uniq # remove duplicates
subjects.shift # get rid of the first option because it's just "All Subjects"
subjects.size == 137
Hope that helps.
Suppose I'm working with an API which returns JSON data, but which has a complex or variable structure. For example, a string-valued property may be a plain literal, or may be tagged with a language:
/* first pattern */
{ "id": 1,
"label": "a foo"
}
/* second pattern */
{ "id": 2,
"label": [ {"value": "a foo", "lang": "en"},
{"value": "un foo", "lang": "fr"}]
}
In my client-side code, I don't want to have view code worrying about whether a label is available in multiple-languages, and which one to pick, etc. Or I might want to hide the detailed JSON structure for other reasons. So, I might wrap the JSON value in an object with a suitable API:
/** Value object for foo instances sent from server */
var Foo = function( json ) {
this.json = json;
};
/** Return a suitable label for this foo object */
Foo.prototype.label = function() {
var i18n = ... ;
if (i18n.prefLang && _.isArray(this.json.label)) // ... etc etc
};
So this is all pretty normal value-object pattern, and it's helpful because it's more decoupled from the specific JSON structure, more testable, etc. OK good.
What I currently don't see a way around is how to use one of these value objects with Backbone and Marionette. Specifically, I'd like to use a Foo object as the basis for a Backbone Model, and bind it to a Marionette ItemView. However, as far as I can see, the values in a Model are taken directly from the JSON structure - I can't see a way to recognise that the objects are functions:
var modelFoo = new Backbone.Model( foo );
> undefined
modelFoo.get( "label" ).constructor
> function Function() { [native code] }
So my question is: what is a good way to decouple the attributes of a Backbone Model from the specifics of a given JSON structure, such as a complex API value? Can value objects, models and views be made to play nice?
Edit
Let me add one more example, as I think the example above focussing on i18n issues only conveys part of my concern. Simplifying somewhat, in my domain, I have waterbodies comprising rivers, lakes and inter-tidal zones. A waterbody has associated with it one or more sampling points, and each sampling point has a latest sample. This might come back from the data API on the server as something like:
{"id": "GB12345678",
"centre": {"lat": 1.2345, "long": "-2.3456"},
"type": "river",
"samplingPoints": [{"id": "sp98765",
"latestSample": {"date": "20130807",
"classification": "normal"}
}]
}
So in my view code, I could write expressions such as:
<%= waterbody.samplingPoints[0].latestSample.classification %>
or
<% if (waterbody.type === "river") { %>
but that would be horrible, and easily broken if the API format changes. Slightly better, I could abstract such manipulations out into template helper functions, but they are still hard to write tests for. What I'd like to do is have a value object class Waterbody, so that my view code can have something like:
<%= waterbody.latestClassification() %>
One of the main problems I'm finding with Marionette is the insistence on calling toJSON() on the models passed to views, but perhaps some of the computed property suggestions have a way of getting around that.
The cleanest solution IMO is to put the label accessor into the model instead of the VO:
var FooModel = Backbone.Model.extend({
getLabel : function(){
return this.getLocalized("label");
},
getLocalized : function(key){
//return correct value from "label" array
}
});
and let the views use FooModel#getLabel instead of FooModel#get("label")
--EDIT 1
This lib seems interesting for your use case as well: Backbone.Schema
It allows you to formally declare the type of your model's attributes, but also provides some syntax sugar for localized strings and allows you to create dynamic attributes (called 'computed properties'), composed from the values of other attributes.
--EDIT 2 (in response to the edited question)
IMO the VO returned from the server should be wrapped inside a model and this model is passed to the view. The model implements latestClassification, not the VO, this allows the view to directly call that method on the model.
A simple approach to this (possibly to simple for your implementation) would be to override the model's parse method to return suitable attributes:
var modelFoo = Backbone.Model.extend({
parse: function ( json ) {
var i18n = ... ;
if (i18n.prefLang && _.isArray(json.label)) {
// json.label = "complex structure"
}
return json;
}
});
That way only your model worries about how the data from the server is formatted without adding another layer of abstraction.
I'm currently building a data generator. First I want to implement is PESEL (kind of personal ID in Poland based on birth date) generator - I want to enter in form a temporary data with start and end birth date interval - I don't want to store it in database (or I should I do it?)
Here is my pesel controller:
def new
#pesel = Array.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #pesel }
end
end
but I've got an "undefined method `model_name' for NilClass:Class" error - is it a good way anyway of solvint this case? I read somewhere that using temporary variables is not with 'The Ruby Way' - if my solution is wrong, please suggest the correct one. (e.g pass this vars through cookies? hash? helper method?)
here is the stacktrace(I think):
Started GET "/pesel" for 127.0.0.1 at 2011-12-05 16:18:20 +0100
Processing by PeselController#new as HTML
Rendered pesel/new.html.erb within layouts/application (1513.9ms)
Completed 500 Internal Server Error in 1793ms
ActionView::Template::Error (undefined method `model_name' for NilClass:Class):
1: <%= simple_form_for #pesel do |f| %>
2: <%= f.input :date_of_birth, :as => :date, :start_year => Date.today.year - 90,
3: :end_year => Date.today.year - 12, :discard_day => true,
4: :order => [:month, :year] %>
app/views/pesel/new.html.erb:1:in `_app_views_pesel_new_html_erb__708648673_90148530'
app/controllers/pesel_controller.rb:7:in `new'
Rendered /home/ofca/.rvm/gems/ruby-1.9.2-p290/gems/actionpack-3.1.1/lib/action_dispatch/middleware/templates/rescues/_trace.erb (5.6ms)
Rendered /home/ofca/.rvm/gems/ruby-1.9.2-p290/gems/actionpack-3.1.1/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (4.0ms)
Rendered /home/ofca/.rvm/gems/ruby-1.9.2-p290/gems/actionpack-3.1.1/lib/action_dispatch/middleware/templates/rescues/template_error.erb within rescues/layout (17.6ms)
form_for assumes certain properties exist for the object you pass it, such as model_name.
Instead of using form_for #pesel, just use form_tag and the related _tag methods.
Use a Pesel model. Models are not tables, and your model doesn't have to write anything to the database. Just don't inherit from ActiveRecord, but do provide a model_name and any other fields the form_for helper expects.