How to upload images to rails api app with react and carrierwave? - ruby-on-rails

I'm trying to upload images to a rails 5 api app with carrierwave and react but for some reason it doesn't work.
Here're some parts of the app.
migration
class AddImagesToTopics < ActiveRecord::Migration[5.1]
def change
add_column :topics, :images, :json
end
end
topic model
class Topic < ApplicationRecord
mount_uploader :images, ImageUploader
...
end
topics controller
def topic_params
params.require(:topic).permit(:name, :options, :subject, :body, {images: []})
end
submitted form logs
#topic = #board.topics.new(topic_params)
p "-----------"
p params[:topic][:images]
p #topic
p "-----------"
gives the following:
Unpermitted parameter: :images
"-----------"
<ActionController::Parameters {"0"=>{}} permitted: false>
#<Topic id: nil, board_id: 5, body: "dsf", created_at: nil, updated_at: nil, name: "", options: "", subject: "", images: nil>
"-----------"
form
class Form extends Component {
constructor(props) {
super(props)
this.state = {
topic: {
name: "",
options: "",
subject: "",
body: "",
images: null
}
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleImagesSelect = this.handleImagesSelect.bind(this)
}
handleSubmit(e) {
e.preventDefault();
this.props.add_topic(this.props.perm, this.state);
}
handleChange(e) {
this.setState({ topic: { ...this.state.topic, [e.target.name]: e.target.value } });
}
handleImagesSelect(e) {
this.setState({ topic: { ...this.state.topic, images: e.target.files } });
console.log(e.target.files);
console.log(this.state);
}
render() {
const { name, options, subject, body, images } = this.state.topic
return(
<div>
<div className="form_wrapper">
<form id="form" onSubmit={this.handleSubmit } encType="multipart/form-data">
<table>
<tbody>
....
<tr>
<td>body</td>
<td>
<textarea name="body" id="body" rows="10" cols="50"
value={body}
onChange={this.handleChange}/>
</td>
</tr>
<tr>
<td>file</td>
<td>
<input name="images" type="file" id="images" multiple="true"
onChange={this.handleImagesSelect}/>
</td>
</tr>
</tbody>
</table>
</form>
</div>
<hr/>
</div>
)
}
}
add topic action
export function add_topic(perm, topic) {
return dispatch => {
axios.post(api + perm + "/topics/", topic)
.then((res) => {
dispatch({ type: ADD_TOPIC, payload: res.data })
})
.catch((err) => console.log(err))
}
}

Related

Rails: direct upload and create has_one_attached parent

I'm making an image library type thing in Rails and Vue, and I'm using DirectUpload to manage attachments.
# photo.rb
class Photo < ApplicationRecord
has_one_attached :file
# ...
end
# photos_controller.rb
class PhotosController < ApplicationController
load_and_authorize_resource
before_action :set_photo, only: %i[show update destroy]
protect_from_forgery unless: -> { request.format.json? }
def index
#photo = current_user.photos.new
render action: 'index'
end
def create
#photo = current_user.photos.create(photo_params)
render json: PhotoBlueprint.render(#photo, root: :photo)
end
# ...
def photo_params
params.require(:photo).permit(:id, :file)
end
end
# photos/index.html.erb
<%= simple_form_for(#photo, url: photos_path(#photo), html: { multipart: true }) do |f| %>
<%= f.file_field :file, multiple: true, direct_upload: true, style: 'display: none;' %>
<% end %>
<div id='photos-app'></div>
// UserFileLib.vue
<script>
import { mapState, mapActions } from 'pinia'
import { usePhotoStore } from '#stores/photo'
import { DirectUpload } from '#rails/activestorage'
export default {
name: 'UserFileLib',
computed: {
...mapState(usePhotoStore, [
'photos'
]),
url () {
return document.getElementById('photo_file').dataset.directUploadUrl
},
input () {
return document.getElementById('file-input')
},
},
mounted () {
this.getPhotos()
},
methods: {
...mapActions(usePhotoStore, [
'addPhoto',
'getPhotos',
]),
activestorageURL (blob) {
return `/rails/active_storage/blobs/redirect/${blob.signed_id}/${blob.filename}`
},
uploadToActiveStorage () {
const file = this.input.files[0]
const upload = new DirectUpload(file, this.url)
upload.create((error, blob) => {
if (error) {
console.log(error)
} else {
const url = this.activestorageURL(blob)
console.log(url)
this.getPhotos()
}
})
},
openFileBrowser () {
this.input.click()
},
formatSize (bytes) {
return Math.round(bytes / 1000)
}
}
}
</script>
<template>
<div
#click="openFileBrowser"
class="card p-3">
Click or drop files here
</div>
<input
type="file"
:multiple="true"
#change="uploadToActiveStorage"
id="file-input" />
<div class="grid is-inline-grid mt-2">
<div
class="image-container"
v-for="image in photos"
:key="image.id">
<img :src="image.url" :alt="image.label" />
<div class="filename">
<strong>{{ image.label }}</strong>
<br />
{{ formatSize(image.size) }} kb
</div>
<div class="close">
×
</div>
</div>
</div>
</template>
Now, the uploads work fine, the blob is stored correctly.
My issue is that a new Photo object is not created to wrap the attachment, meaning the uploads are lost in the system and have no parent record.
What am I doing wrong?
I've solved this for anyone else looking for help. The logic is to create or update the parent record after the upload is done. I missed this in the official documentation.
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// ** This is the way **
// Add an appropriately-named hidden input to the form with a
// value of blob.signed_id so that the blob ids will be
// transmitted in the normal upload flow
// ** End of **
//
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
Since I'm using Vue and Pinia I made a solution in keeping with that logic:
// UserImageLib.vue
uploadToActiveStorage (input, file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log(error)
} else {
const params = { [input.name]: blob.signed_id }
this.createPhoto(params)
}
})
},
// stores/photo.js
addPhoto (payload) {
this.photos.unshift(payload)
},
createPhoto (payload) {
http.post(`/photos`, payload).then(res => {
const photo = res.data.photo
this.addPhoto(photo)
})
},

【Vue.js】want to「 Error in render: "TypeError: Cannot read property 'title' of undefined" 」

I am creating a detail screen with the app I am creating.
When transitioning to the detail screen, the contents of the data do not occur on the screen, and this error occurs in the console:
Error in render: "TypeError: Cannot read property 'title' of undefined".
code
<template>
<main>
<section>
<div>
<table>
<thead>
<tr>
<th>タイトル</th>
<th>詳細</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ latest_information.title }}</td>
<td>{{ latest_information.detail }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</template>
<script>
export default {
data() {
return {
latest_information: {},
}
},
methods: {
setData() {
this.$loading.load(this.$auth.api.get(`admin/latest_informations/${this.$route.params.id}.json`)
.then(response => {
this.latest_information = response.data.latest_information
})
.catch(err => {
this.$errorHandlers.initial(err);
}));
},
},
mounted() {
this.setData();
},
}
</script>
command「rails routes」↓
api_v1_admin_latest_information GET /api/v1/admin/latest_informations/:id(.:format) api/v1/admin/latest_informations#show {:format=>:json}
latest_informations_controller.rb
module Api::V1::Admin
class LatestInformationsController < Api::V1::ApiController
before_action :set_latest_information, only: [:show, :destroy]
def show
render json: #latest_information
end
private
def set_latest_information
#latest_information = LatestInformation.find(params[:id])
end
end
end
show.json.jbuilder
json.set! :latest_information do
json.id #latest_information.id
json.title #latest_information.title
json.detail #latest_information.detail
end
Tried
・Put byebug in the controller,check the value of #latest_information
(byebug) #latest_information
#<LatestInformation id: 3, title: "���������", detail: "���������", created_at: "2021-08-01 22:01:39", updated_at: "2021-08-01 22:01:39">
・Check the JSON data created by jbuilder
irb(main):040:0> Jbuilder.encode do |json|
irb(main):041:1* json.set! :latest_information do
irb(main):042:2* json.id LatestInformation.find(3).id
irb(main):043:2> json.title LatestInformation.find(3).title
irb(main):044:2> json.detail LatestInformation.find(3).detail
irb(main):045:2> end
irb(main):046:1> end
LatestInformation Load (1.3ms) SELECT `latest_informations`.* FROM `latest_informations` WHERE `latest_informations`.`id` = 3 LIMIT 1
LatestInformation Load (1.0ms) SELECT `latest_informations`.* FROM `latest_informations` WHERE `latest_informations`.`id` = 3 LIMIT 1
LatestInformation Load (0.7ms) SELECT `latest_informations`.* FROM `latest_informations` WHERE `latest_informations`.`id` = 3 LIMIT 1
=> "{\"latest_information\":{\"id\":3,\"title\":\"てst\",\"detail\":\"てst\"}}"
Environment
rails 6
vue#2.6.10
postscript
Look at the contents of response and modify as follows.I got the value.
However, it is not displayed on the screen.
Show.vue
data() {
return {
latest_information: {
title: "",
detail: ""
},
}
},
methods: {
// api_v1_admin_latest_information GET /api/v1/admin/latest_informations/:id(.:format) api/v1/admin/latest_informations#show {:format=>:json}
setData() {
this.$loading.load(this.$auth.api.get(`admin/latest_informations/${this.$route.params.id}.json`)
.then(response => {
this.title = response.data.title
this.detail = response.data.detail
})
.catch(err => {
this.$errorHandlers.initial(err);
}));
},
},
mounted() {
this.setData();
},
}
I think this one is the cause
.then(response => {
this.title = response.data.title
this.detail = response.data.detail
})
it should
.then(response => {
this.title = response.latest_information.title
this.detail = response.latest_information.detail
})
or you can try to debug the response
.then(response => {
console.log(response)
})

Creating a Review Rails Backend/React Frontend

I am attempting to allow a user that is logged in to create a review for a game. I am having a couple issues that keep popping up in my console.
HTTP Origin header (http://localhost:3001) didn't match request.base_url (http://localhost:3000)
I attempted to remedy this with putting config.force_ssl = true in my production file from what I read up on, but still hitting this issue.
Im also hitting
NoMethodError (undefined method `id' for nil:NilClass):
which is ref my review_controller in create
app/controllers/reviews_controller.rb:19:in `create'
Below is my ReviewController and my ReviewContainer and ReviewForm
class ReviewsController < ApplicationController
# before_action :authorized, only:[:create]
before_action :authenticate_with_http_digest, only: [:new, :create]
def index
reviews = Review.all
render json: reviews
end
def show
review = Review.find_by(params[:id])
render json: review
end
def create
game = Game.find_or_create_by(name: params[:review][:game_name])
review = Review.new(review_params)
review.game_id = game.id
review.user_id = #user.id
review.save
render json: review
end
def update
review = Review.find(params[:id])
review.update(review_params)
review.save
render json: review
end
def destroy
review = Review.find(params[:id])
review.destroy
render json: {error: "Review Removed"}
end
private
def review_params
params.require(:review).permit(:user_id, :game_id, :user_username, :reviewed_game, :rating, :game_name)
end
end
import React, { Component } from 'react'
import Review from './Review'
import ReviewForm from './ReviewForm'
export default class ReviewsContainer extends Component {
state = {
reviews: [],
}
componentDidMount(){
fetch('http://localhost:3000/reviews')
.then(res => res.json())
.then(reviews => this.setState({ reviews }))
}
addReview = (review) => {
fetch('http://localhost:3000/reviews',{
method: "POST",
headers: {
"Content-Type" : "application/json",
Accept: "application/json",
Authorization: `bearer ${localStorage.token}`
},
body: JSON.stringify({ review: review }
),
})
.then(res => res.json())
.then(( json => {
this.setState(prevState => ({
reviews: [...prevState.reviews, json ]
}))
}))
}
// handleShowForm = () => {
// this.setState({edit: false})
// }
render() {
return (
<div className="review-grid">
<ReviewForm addReview={this.addReview} review={this.handleSubmit} />
<h1 className="review-content">REVIEWS!</h1>
<ul className="review-cards">
{
this.state.reviews.map(review => <Review key={review.id} review={review}/>)
}
</ul>
</div>
)
}
}
import React, { Component } from 'react'
class ReviewForm extends React.Component {
state = {
reviewed_game: '',
rating: '',
user_username: '',
}
handleReviewedGame = (event) => {
this.setState ({
reviewed_game: event.target.value
})
}
handleRating = (event) => {
this.setState ({
rating: event.target.value
})
}
handleUser = (event) => {
this.setState ({
user_username: event.target.value
})
}
handleForm = (e) => {
e.preventDefault()
// console.log(e)
const review = {
reviewed_game: this.state.reviewed_game,
rating: this.state.rating,
}
this.props.addReview(review)
}
render() {
return (
<div className="form-container">
<form onSubmit={(e) => {this.handleForm(e)}}>
<div>
<label>Review</label>
<br></br>
<textarea type="text" placeholder="Drop Your Review" rows={10} cols={50} value={this.state.reviewed_game} onChange={this.handleReviewedGame} className="form"/>
<div>
<label>Stars</label>
<br></br>
<input type="number" max="5" min="0" value={this.state.rating} onChange={this.handleRating} />
</div>
</div>
<button type="submit" className="sub-review">Create Review!</button>
</form>
</div>
)
}
}
export default ReviewForm;
Any advise on how to correct the issue is appreciated! thanks!

Rails post controller not passing user association to React view

I'm trying to make a basic CRUD store app with Rails and React, but I'm stuck on displaying the author (user) association of the post. The post itself shows just fine. I'm trying to avoid using jbuilder so I can understand the problem I'm having.
The current show method in the controller, which works:
controllers/post_controller.rb
def show
if post
render json: post
else
render json: post.errors
end
end
The current React view, which works:
app/javascript/components/Post.js
import React from "react";
import { Link } from "react-router-dom";
class Post extends React.Component {
constructor(props) {
super(props);
this.state = { post: { description : '' } };
}
componentDidMount() {
const {
match: {
params: { id }
}
} = this.props;
const url = `/api/v1/show/${id}`;
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(response => this.setState({ post: response }))
.catch(() => this.props.history.push("/posts"));
}
render() {
const { post } = this.state;
let descriptionList = "No descriptions present";
if (post.description.length > 0) {
descriptionList = post.description
.split(",")
.map((description, index) => (
<li key={index} className="list-group-item">
{description}
</li>
));
}
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={post.image}
alt={`${post.description} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Description</h5>
{descriptionList}
<div>{post.title}</div>
<div>${(post.price * .01).toLocaleString()}</div>
</ul>
</div>
</div>
<Link to="/posts" className="btn btn-link">
Back to all posts
</Link>
</div>
</div>
);
}
}
export default Post;
When I add render json: post, include: :user to the controller and {post.user.email} and render() { const { post, user } = this.state;
to the view, the error message in the console is cannot read property 'email' of undefined. When I try to define the user in the controller method user = post.user.email and in the view {user}, the terminal error is:
NoMethodError (undefined method 'oswaldo#daugherty.info' for #<Post id: 5, title: "Post 5", description: "You can't synthesize the bandwidth without compres...", image: "https://loremflickr.com/300/300/cats 5", price: 883105, rating: nil, review: nil, created_at: "2021-01-31 23:26:03", updated_at: "2021-01-31 23:26:03", user_id: 5>):
I've checked my database and all the associations display correct there. In short, I don't know how to send the post's user association correctly to the view. What am I missing? Any help appreciated because I'm really spinning my wheels on this one.
you might be facing the bug reported bug.
If you are only looking email from related user record, you can use following
# in Post model
delegate :email, to: :user, prefix: true, allow_nil: true
# and while rendering
use Post.as_json(methods: [:user_email])

Unpermitted parameter: :recipe

I am having an issue with submitting my data from form on the front end. Everytime I submit the form I get a Unpermitted parameter: :recipe
I was told to make sure my attributes on my frontend matched what was on the backend strong params.
Here is what is in my controller for my create action and my strong params
class RecipesController < ApplicationController
def create
recipe = Recipe.create(recipe_params)
if recipe.save
render json: recipe
else
render json: { error: "Couldn't save" }
end
end
private
def recipe_params
params.permit(:category_id,:name,:ingredients,:chef_name,:origin,category_attribute:[:category])
end
end
And here is my React frontend where I am inputting the info in the form
Side note I took out the event handlers for this code snippet but left the submit handler just to keep this explanation shorter
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
categoryId: '',
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
this.handleNameChange.bind(this)
this.handleOriginChange.bind(this)
this.handleChefChange.bind(this)
this.handleIngChange.bind(this)
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGOREIS_URL =`${BASE_URL}/categories`
fetch(CATEGOREIS_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
console.log(initialCats)
this.setState({
category: initialCats,
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
categoryId: '',
name:'',
ingredients: '',
chef_name: '',
origin: ''
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
I am just a little clueless on where to go to from here. Am I matching the attributes correctly?
Edit
I forgot to include my postRecipes function with my dispatches in place
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response =>
response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
categoryId: resp.categoryId
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
Edit
Here is my reducer that defines my Add-Recipe action for payload.
export default function manageRecipes(state={
recipes:[],
category:[],
}, action){
switch(action.type){
case 'Add_Recipe':
const recipe = {
name: action.name,
ingredients: action.ingredients,
chef_name: action.chef_name,
origin: action.origin,
categoryId: action.categoryId,
category: action.category,
// id: cuidFn()
}
return{
...state,
recipes: [...state.recipes, recipe],
}
case 'Delete_Recipe':
const recipes = state.recipes.filter(recipe => recipe.id !== action.id)
return {...state, recipes}
case 'Add_Catagory':
const cat = {
name: action.name
}
return{
...state,
category: [...state.category, cat],
}
default:
return state
}
}
try this in your backend
def recipe_params
params.require(:recipe).permit(
:category_id, :name,:ingredients, :chef_name,
:origin,category_attribute:[:category]
)
end

Resources