using react and json to upload images to rails - ruby-on-rails

I have a form in a react compnent that includes an image. This component has an associated state for the form, it is quite a large and complex form with many nested arrays of sub elements that vary in length, which is why I do not want to use FormData().
state = {
recipe: {
title: "",
image: null,
ingredient_groups: [
{
title: "",
ingredients: [
{
name: "",
measurement_metric: "",
measurement_imperial: ""
}
]
}
]
}
The state is updated with an onChange handler for the input field like the following
let {recipe} = this.state;
recipe.image = event.target.files[0];
this.setState({recipe});
This is then sent via an axios post request,
let {recipe} = this.state;
axios.post('/api/v1/recipe', {recipe: recipe}).then((result) => {
console.log(result.status)
})
in rails I have a params method set up for the allowed params
def recipe_params
params.require(:recipe).permit(:title,
:image,
{ingredient_groups: [
:id,
:title,
{ingredients: [
:id,
:name,
:measurement_metric,
:measurement_imperial
]}
]})
end
however I am getting nil for recipe_params[:image] when I try to save it into active storage like this:
#recipe.image.attach(recipe_params[:image])
Is what Im doing even possible? Or is the only way to transmit this via a FormData object on the post. I would really prefer this to be done via a pure json method.

using https://www.npmjs.com/package/object-to-formdata I was able to successfully change my react state object into a FormData Object, and then post this to my endpoint and it has worked as expected

Related

How to upload local image file on React Native app to Rails api?

I'm having trouble understanding how a local file path from a smartphone could possibly get uploaded on the server side with a Rails api for instance.
The file path that we're sending to the backend doesn't mean anything to the server?
I'm getting a uri from the response like this:
file:///Users/.../Documents/images/5249F841-388B-478D-A0CB-2E1BF5511DA5.jpg):
I have tried to send something like this to the server:
let apiUrl = 'https://vnjldf.ngrok.io/api/update_photo'
let uriParts = uri.split('.');
let fileType = uri[uri.length - 1];
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});
let options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
But I'm unsure what it is and how to decript it on the backend.
I have also tried sending the uri direclty but of course I'm getting the following error:
Errno::ENOENT (No such file or directory # rb_sysopen -...
Any help/guidance would be much appreciated.
I have recently spent 1+ hour debugging something similar.
I found out that if you make a POST to your Rails backend from your React Native app using this json:
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileName}`,
type: `image/${fileType}`,
});
Rails will automatically give you a ActionDispatch::Http::UploadedFile in your params[:photo], which you can attach directly to your model like Photo.create(photo: params[:photo]) and it simply works.
However, if you don't pass a filename, everything breaks and you'll get a huge string instead and it will raise a ArgumentError (invalid byte sequence in UTF-8).
So, based on your code, I can spot the bug right on: you are passing name as photo.${fileType}, which is wrong, and should be photo.${fileName} (update accordingly to get your image filename ... console.log(photo) in your React Native code will show you the correct one.
Maintain issues with deleting and adding new files
This is how I managed to do it add multiple file upload and maintain issues with deleting and adding new files
class User < ApplicationRecord
attribute :photos_urls # define it as an attribute so that seriallizer grabs it to generate JSON i.e. as_json method
has_many_attached :photos
def photos_urls
photos.map do |ip|
{url: Rails.application.routes.url_helpers.url_for(ip), signed_id: ip.signed_id}
end
end
See about signed_id here. It describes how you can handle multiple file upload.
Controller looks like
def update
user = User.find(params[:id])
if user.update(user_params)
render json: {
user: user.as_json(except: [:otp, :otp_expiry])
}, status: :ok
else
render json: { error: user.errors.full_messages.join(',') }, status: :bad_request
end
end
...
private
def user_params
params.permit(
:id, :name, :email, :username, :country, :address, :dob, :gender,
photos: []
)
end
React Native part
I am using react-native-image-crop-picker
import ImagePicker from 'react-native-image-crop-picker';
...
const photoHandler = index => {
ImagePicker.openPicker({
width: 300,
height: 400,
multiple: true,
}).then(selImages => {
if (selImages && selImages.length == 1) {
// Make sure, changes apply to that image-placeholder only which receives 'onPress' event
// Using 'index' to determine that
let output = images.slice();
output[index] = {
url: selImages[0].path, // For <Image> component's 'source' field
uri: selImages[0].path, // for FormData to upload
type: selImages[0].mime,
name: selImages[0].filename,
};
setImages(output);
} else {
setImages(
selImages.map(image => ({
url: image.path, // For <Image> component's 'source' field
uri: image.path, // for FormData to upload
type: image.mime,
name: image.filename,
})),
);
}
});
};
...
<View style={style.imageGroup}>
{images.map((item, index) => (
<TouchableOpacity
key={`img-${index}`}
style={style.imageWrapper}
onPress={() => photoHandler(index)}>
<Image style={style.tileImage} source={item} />
</TouchableOpacity>
))}
</View>
Uploader looks like
// ../models/api/index.js
// Update User
export const updateUser = async ({ id, data }) => {
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
let formData = new FormData(data);
for (let key in data) {
if (Array.isArray(data[key])) {
// If it happens to be an Image field with multiple support
for (let image in data[key]) {
if (data[key][image]?.signed_id) {
// if the data has not change and it is as it was downloaded from server then
// it means you do not need to delete it
// For perverving it in DB you need to send `signed_id`
formData.append(`${key}[]`, data[key][image].signed_id);
} else if (data[key][image]?.uri && data[key][image]?.url) {
// if the data has change and it is as it has been replaced because user selected a different image in place
// it means you need to delete it and replace it with new one
// For deleting it in DB you should not send `signed_id`
formData.append(`${key}[]`, data[key][image]);
}
}
} else {
formData.append(key, data[key]);
}
}
return axios.patch(BASE_URL + "/users/" + data.id, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
and Saga worker looks like
import * as Api from "../models/api";
// worker Saga:
function* updateUserSaga({ payload }) {
console.log('updateUserSaga: payload', payload);
try {
const response = yield call(Api.updateUser, {
id: payload.id,
data: payload,
});
if (response.status == 200) {
yield put(userActions.updateUserSuccess(response.data));
RootNavigation.navigate('HomeScreen');
} else {
yield put(userActions.updateUserFailure({ error: response.data.error }));
}
} catch (e) {
console.error('Error: ', e);
yield put(
userActions.updateUserFailure({
error: "Network Error: Could not send OTP, Please try again.",
})
);
}
}

Angular 4 & Rails 5 Post Request JSON Formatting

I'm developing an angular app using a rails backend. I'm having problems formatting the parameters hash so rails can use it. The data is a many to many relationship, and the form contains nested attributes. In Rails, my models utilize the accepts_nested_attributes_for helper. I know exactly what format rails expects, but when I make a POST request, there is one minor detail that's off. I'm going to list below two param hashes. One is what Angular produces, and the other is what Rails expects.
What's off about the Angular request is rails expects a deeper layer of nesting in the expense_expense_categories attributes. I've never understood why rails requires it. What angular produces looks logical to me. My question is.. What do I need to do to format the parameters in Angular? Looking at what I have so far, am I doing this in a way that satisfies Angular best practices?
Angular:
{
"expense": {
"date": "2017/4/13",
"check_number": "132",
"debit": "0",
"notes": "har",
"amount": "24",
"payee_id": "334"
},
"expense_expense_categories_attributes": [{
"expense_category_id": "59",
"amount": 12
},
{
"expense_category_id": "62",
"amount": 11
}
]
}
What Rails expects:
{
"expense": {
"date": "2017/12/12",
"check_number": "122",
"debit": "0",
"notes": "har",
"amount": "24",
"payee_id": "334",
"expense_expense_categories_attributes": {
"210212312": {
"expense_category_id": "72",
"amount": "12"
},
"432323432": {
"expense_category_id": "73",
"amount": "12"
}
}
}
}
My code in angular is as follows.
onSubmit() method in component:
onSubmit() {
this.expenseService.addExpense(this.expenseForm.value)
.subscribe(
() => {
this.errorMessage = '';
},
error => {
this.errorMessage = <any>error;
}
);
this.expenseForm.reset();
}
addExpense in my service file:
addExpense(expense: Expense): Observable<any> {
let headers = new Headers({'Content-Type': 'application/json'});
let options = new RequestOptions({headers: headers});
return this.http.post('http://localhost:3000/expenses', expense, options)
.map(
(res: Response) => {
const expenseNew: Expense = res.json();
this.expenses.push(expenseNew);
this.expensesChanged.next(this.expenses.slice());
})
.catch(this.handleError);
}
my main form:
private initForm() {
let expense_expense_categories_attributes = new FormArray([]);
this.expenseForm = this.fb.group({
id: '',
date: '',
amount: '',
check_number: '',
debit: '',
payee_id: '',
notes: '',
expense_expense_categories_attributes: expense_expense_categories_attributes
});
}
My FormArray for nested attributes:
onAddExpenseCategories() {
(<FormArray>this.expenseForm.get('expense_expense_categories_attributes')).push(
new FormGroup({
'expense_category_id': new FormControl(null, Validators.required),
'amount': new FormControl(null, [
Validators.required
])
})
);
}
UPDATE: I was able to get it working, but I had to use a god awful regex to manipulate the request to what I wanted. It was an extremely ugly option so I still need to find a better option. Is there a better way to format JSON Objects and replace the contents? I'm not sure the correct way to do it. Need help.
You need to add the expense_expense_categories to the wrap_parameters like this:
wrap_parameters :expense, include: [:expense_expense_categories_attributes]
Additional attributes must be explicitly added to wrap_parameters as it only wraps attributes of the model itself by default.

angular2 formBuilder group causes nesting of params

I have the following formBuilder in angular2:
constructor(
private formBuilder: FormBuilder) {
this.form = formBuilder.group({
id: [],
title: ['', Validators.required],
dates: formBuilder.group({
start_date: ['', Validators.required],
end_date: ['', Validators.required]
}, {validator: this.checkDates})
});
}
dates is in a separate group, this is for validation purposes. onSubmit calls this service method:
update(academicTerm: AcademicTerm): Observable<AcademicTerm> {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
return this.http
.patch(this.endpointUrl + academicTerm.id, JSON.stringify(academicTerm), {headers})
.map(this.extractData)
.catch(this.handleError);
}
When I check the backend (Rails5 API server) I can see this param set:
Parameters: {"id"=>"3", "title"=>"Term Title", "dates"=>{"start_date"=>"2016-11-27", "end_date"=>"2016-12-01"}, "academic_term"=>{"id"=>"3", "title"=>"Term CL"}}
Note in the academic_term hash that start_date and end_date are not present.
On the Rails side of things I have strong params set up like this:
def academic_term_params
params.require(:academic_term).permit(:id, :title, :start_date, :end_date)
end
I have tried setting the nested dates object in strong params:
def academic_term_params
params.require(:academic_term).permit(:id, :title, :dates => [:start_date, :end_date])
end
Which has no affect (dates is not an associated attribute?). So while I can update title I cannot update the dates.
Is there a way to flatten the params sent from angular to be something like this:
Parameters: {"id"=>"3", "title"=>"Term Title", "start_date"=>"2016-11-27", "end_date"=>"2016-12-01"}
Or is there a way to fix it on the Rails side?
You can flatten the object before sending the request to the server.
update(academicTerm: AcademicTerm): Observable<AcademicTerm> {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
academicTerm['start_date'] = academicTerm.dates.start_date;
academicTerm['end_date'] = academicTerm.dates.end_date;
// delete academicTerm.dates; // optional
return this.http
.patch(this.endpointUrl + academicTerm.id, JSON.stringify(academicTerm), {headers})
.map(this.extractData)
.catch(this.handleError);
}

Custom envelope fields using Docusign REST API

I'm using the docusign_rest gem to integrate with the Docusign REST API in my rails application. I have created a custom envelope field in the Docusign admin called SFID. I need to pass an ID into SFID inside of the envelope. I'm getting the following error with my JSON code:
{"errorCode"=>"INVALID_REQUEST_BODY", "message"=>"The request body is missing or improperly formatted. Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'API_REST.Models.v2.customFields' because the type requires a JSON object (e.g. {\"name\":\"value\"}) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON object (e.g. {\"name\":\"value\"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.\r\nPath 'customFields', line 1, position 1073."}
My controller code:
#envelope_response = client.create_envelope_from_template(
status: 'sent',
email: {
subject: "The test email subject envelope",
body: ""
},
template_id: '90B58E8F-xxxxx',
custom_fields: [
{
textCustomFields: [
{
name: 'SFID',
value:'12345',
required: 'false',
show: 'true'
}
]
}
],
signers: [
...
The Docusign API explorer says the following is the correct way to push an envelope custom field:
{
"customFields": {
"textCustomFields": [
{
"value": "0101010101",
"required": "true",
"show": "true",
"name": "SFID"
},
{
"required": "true",
"show": "true"
}
]
}
}
The Docusign_rest gem says the following on custom envelope fields:
customFields - (Optional) A hash of listCustomFields and textCustomFields.
# Each contains an array of corresponding customField hashes.
# For details, please see: http://bit.ly/1FnmRJx
What formatting changes to I need to make to my controller code to get it to successfully push a custom envelope field?
You have an extra array in your customFields node.
Remove the [] array from your custom_fields:
#envelope_response = client.create_envelope_from_template(
status: 'sent',
email: {
subject: "The test email subject envelope",
body: ""
},
template_id: '90B58E8F-xxxxx',
custom_fields:
{
textCustomFields: [
{
name: 'SFID',
value:'12345',
required: 'false',
show: 'true'
}
]
},
signers: [
...
Also I'm assuming that your client.create_envelope_from_template is converting your _'s into a camelCased string. if that is not happening, then that also needs to change.

How to pass a Hash to Grape API method?

I'm having problems with the Grape gem and the parameters validation.
The idea behind this is to create a complex entity using nested attributes through an API service.
I have a method to create a trip, trip have many destinations and i want to pass that destinations using a hash (using the accepts_nested_attributes_for helper).
I have this grape restriction over the parameter:
requires :destinations, type: Hash
And I'm trying to send something like this:
{ destinations => [
{ destination: { name => 'dest1'} },
{ destination: { name => 'dest2'} },
{ destination: { name => 'dest3'} }
]}
In order to build something like the structure below inside the method and get the trip created:
{ trip: {
name: 'Trip1', destinations_attributes: [
{ name: 'dest1' },
{ name: 'dest2' },
{ name: 'dest3' }
]
}}
I'm using POSTMAN chrome extension to call the API method.
Here's a screen capture:
If someone can help me i would be very grateful.
By the looks of what you are trying to send, you need to change the Grape restriction, because destinations is an Array, not a Hash:
requires :destinations, type: Array
You don't need the "destination" hash when sending the request:
{ destinations => [
{ name => 'dest1', other_attribute: 'value', etc... },
{ name => 'dest2', other_attribute: 'value', etc... },
{ name => 'dest3', other_attribute: 'value', etc... }
]}
This creates an Array of hashes.
In order to send this through POSTMAN, you'll need to modify that destinations param your sending and add multiple lines in POSTMAN. Something like:
destinations[][name] 'dest1'
destinations[][other_attribute] 'value1'
destinations[][name] 'dest2'
destinations[][other_attribute] 'value2'
destinations[][name] 'dest3'
destinations[][other_attribute] 'value3'
Hope this answers your questions. Let me know if this is what you were looking for.

Resources