2016-04-15 15 views
8

Ho due controller product nel mio back-end elisir/phoenix. In primo luogo - API endpoint (pipe_through :api) e secondo controller piping through :browser:Allegati di file per redux-form ed elisir/phoenix come API di backend (problema di serializzazione)

# router.ex 
scope "/api", SecretApp.Api, as: :api do 
    pipe_through :api 

    resources "products", ProductController, only: [:create, :index] 
end 

scope "/", SecretApp do 
    pipe_through :browser # Use the default browser stack 

    resources "products", ProductController, only: [:new, :create, :index] 
end 

ProductController gestisce le richieste provenienti forma generate da helper delle form elisir e accetta un certo file allegati. Va tutto bene. Qui è creare l'azione e params elaborati da questa azione:

def create(conn, %{"product" => product_params}) do 
    changeset = Product.changeset(%Product{}, product_params) 

    case Repo.insert(changeset) do 
    {:ok, _product} -> 
     conn 
     |> put_flash(:info, "Product created successfully.") 
     |> redirect(to: product_path(conn, :index)) 
    {:error, changeset} -> 
     render(conn, "new.html", changeset: changeset) 
    end 
end 

params da log (sto usando arc per la movimentazione di caricamento delle immagini nel codice elisir)

[debug] Processing by SecretApp.ProductController.create/2 
    Parameters: %{"_csrf_token" => "Zl81JgdhIQ8GG2c+ei0WCQ9hTjI+AAAA0fwto+HMdQ7S7OCsLQ9Trg==", "_utf8" => "✓", 
       "product" => %{"description" => "description_name", 
       "image" => %Plug.Upload{content_type: "image/png", 
        filename: "wallpaper-466648.png", 
        path: "/tmp/plug-1460/multipart-754282-298907-1"}, 
       "name" => "product_name", "price" => "100"}} 
    Pipelines: [:browser] 

Api.ProductController gestisce le richieste provenienti da redux-from. Ecco l'azione, vista e params, che vengono elaborati da questa azione:

# action in controller 
def create(conn, %{"product" => product_params}) do 
    changeset = Product.changeset(%Product{}, product_params) 

    case Repo.insert(changeset) do 
    {:ok, _product} -> 
     conn 
     |> render("index.json", status: :ok) 
    {:error, changeset} -> 
     conn 
     |> put_status(:unprocessable_entity) 
     |> render("error.json", changeset: changeset) 
    end 
end 

# product_view.ex 
def render("index.json", resp=%{status: status}) do 
    %{status: status} 
end 

def render("error.json", %{changeset: changeset}) do 
    errors = Enum.into(changeset.errors, %{}) 

    %{ 
    errors: errors 
    } 
end 

[info] POST /api/products/ 
[debug] Processing by SecretApp.Api.ProductController.create/2 
    Parameters: %{"product" => %{"description" => "product_description", "image" => "wallpaper-466648.png", "name" => "product_name", "price" => "100"}} 
    Pipelines: [:api] 
[info] Sent 422 in 167ms 

creano l'azione non riesce con 422 lo stato, perché l'immagine non può essere salvata con queste params. Il mio problema è che non riesco ad accedere all'immagine dal codice back-end, l'ho solo nel mio codice JS come oggetto FileList. Non capisco come passare l'immagine al codice back-end. Ecco come questo allegato è rappresentato nel mio codice JS (FileList, contenente informazioni sull'immagine caricata).

value:FileList 
    0: File 
    lastModified: 1381593256801 
    lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300 
    name: "wallpaper-466648.png" 
    size: 1787293 
    type: "image/png" 
    webkitRelativePath: "" 

ho solo WebkitRelativePath (In caso di primo controller devo percorso dell'immagine: "/ tmp/plug-1460/multipart-754282-298907-1") e non so che cosa posso fare con questo oggetto JS e come accedere all'immagine reale rappresentata da questo oggetto JS (ecco uno redux-form reference sui caricamenti di file).

Potrebbe aiutarmi? Come spiegare ad elisir come trovare un'immagine? Vorrei solo inviare file allegati al mio backend usando il codice JS (perché ci sono molte caratteristiche interessanti per la validazione asincrona, ecc.).

Ecco un link ad una completa app se potesse essere utile

+0

Le risorse statiche come le immagini devono essere visualizzate in 'priv/static /'. Riesci a trovare il file in 'priv/static/tmp/plug-1460/multipart-754282-298907-1'? In caso contrario, prova a memorizzare l'immagine lì. – tkowal

+0

Il mio problema è che non riesco ad accedere all'immagine dal codice back-end, l'ho solo nel mio codice JS come oggetto FileList. Non capisco come passare l'immagine al codice back-end. Non c'è niente in priv/static/tmp comunque –

risposta

3

Finalmente sono riuscito a risolvere questo problema. La soluzione è nella corretta serializzazione dei parametri inviati da redux-form.

Qui è la mia forma redux, punto di domanda di partenza:

// product_form.js 

import React, { PropTypes } from 'react'; 
import {reduxForm} from 'redux-form'; 

class ProductForm extends React.Component { 
    static propTypes = { 
    fields: PropTypes.object.isRequired, 
    handleSubmit: PropTypes.func.isRequired, 
    error: PropTypes.string, 
    resetForm: PropTypes.func.isRequired, 
    submitting: PropTypes.bool.isRequired 
    }; 

    render() { 
    const {fields: {name, description, price, image}, handleSubmit, resetForm, submitting, error} = this.props; 

    return (
     <div className="product_form"> 
     <div className="inner"> 
      <form onSubmit={handleSubmit} encType="multipart/form-data"> 
      <div className="form-group"> 
       <label className="control-label"> Name </label> 
       <input type="text" className="form-control" {...name} /> 
       {name.touched && name.error && <div className="col-xs-3 help-block">{name.error}</div>} 
      </div> 

      <div className="form-group"> 
       <label className="control-label"> Description </label> 
       <input type="textarea" className="form-control" {...description} /> 
       {description.touched && description.error && <div className="col-xs-3 help-block">{description.error}</div>} 
      </div> 

      <div className="form-group"> 
       <label className="control-label"> Price </label> 
       <input type="number" step="any" className="form-control" {...price} /> 
       {price.touched && price.error && <div className="col-xs-3 help-block">{price.error}</div>} 
      </div> 

      <div className="form-group"> 
       <label className="control-label"> Image </label> 
       <input type="file" className="form-control" {...image} value={ null } /> 
       {image.touched && image.error && <div className="col-xs-3 help-block">{image.error}</div>} 
      </div> 

      <div className="form-group"> 
       <button type="submit" className="btn btn-primary" >Submit</button> 
      </div> 
      </form> 
     </div> 
     </div> 
    ); 
    } 
} 

ProductForm = reduxForm({ 
    form: 'new_product_form', 
    fields: ['name', 'description', 'price', 'image'] 
})(ProductForm); 

export default ProductForm; 

Questa forma passa i seguenti params alla funzione handleSubmit dopo che l'utente preme il pulsante "Invia"

# values variable 
Object {name: "1", description: "2", price: "3", image: FileList} 

# where image value is 
value:FileList 
    0: File 
    lastModified: 1381593256801 
    lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300 
    name: "wallpaper-466648.png" 
    size: 1787293 
    type: "image/png" 
    webkitRelativePath: "" 

Per passare questi parametri al backend sto usando il FormData Web API e lo file-upload request using isomorphic-fetch npm module

Ecco il codice ha fatto il tri ck:

// product_form_container.js (where form submit processed, see _handleSubmit function) 

import React     from 'react'; 
import ProductForm    from '../components/product_form'; 
import { Link }    from 'react-router'; 
import { connect }    from 'react-redux'; 
import Actions     from '../actions/products'; 
import * as form_actions   from 'redux-form'; 
import {httpGet, httpPost, httpPostForm} from '../utils'; 

class ProductFormContainer extends React.Component { 
    _handleSubmit(values) { 
    return new Promise((resolve, reject) => { 
     let form_data = new FormData(); 

     Object.keys(values).forEach((key) => { 
     if (values[key] instanceof FileList) { 
      form_data.append(`product[${key}]`, values[key][0], values[key][0].name); 
     } else { 
      form_data.append(`product[${key}]`, values[key]); 
     } 
     }); 

     httpPostForm(`/api/products/`, form_data) 
     .then((response) => { 
     resolve(); 
     }) 
     .catch((error) => { 
     error.response.json() 
     .then((json) => { 
      let responce = {}; 
      Object.keys(json.errors).map((key) => { 
      Object.assign(responce, {[key] : json.errors[key]}); 
      }); 

      if (json.errors) { 
      reject({...responce, _error: 'Login failed!'}); 
      } else { 
      reject({_error: 'Something went wrong!'}); 
      }; 
     }); 
     }); 
    }); 
    } 

    render() { 
    const { products } = this.props; 

    return (
     <div> 
     <h2> New product </h2> 
     <ProductForm title="Add product" onSubmit={::this._handleSubmit} /> 

     <Link to='/admin/products'> Back </Link> 
     </div> 
    ); 
    } 
} 

export default connect()(ProductFormContainer); 

dove httpPostForm è un wrapper fetch:

export function httpPostForm(url, data) { 
    return fetch(url, { 
    method: 'post', 
    headers: { 
     'Accept': 'application/json' 
    }, 
    body: data, 
    }) 
    .then(checkStatus) 
    .then(parseJSON); 
} 

E questo è tutto. Non c'era niente da sistemare nel mio codice di elisir, Api.ProductController rimane lo stesso (vedi il post iniziale). Ma ora riceve la richiesta con i seguenti parametri:

[info] POST /api/products/ 
[debug] Processing by SecretApp.Api.ProductController.create/2 
    Parameters: %{"product" => %{ 
       "description" => "2", 
       "image" => %Plug.Upload{ 
        content_type: "image/jpeg", 
        filename: "monkey_in_jungle-t3.jpg", 
        path: "/tmp/plug-1461/multipart-853391-603088-1" 
       }, 
       "name" => "1", 
       "price" => "3"}} 
    Pipelines: [:api] 

Mille grazie per tutti che cercano di aiutarmi. Spero che questo possa aiutare qualcuno che sta lottando con problemi di serializzazione simili.

2

dal registro, è chiaro che l'immagine sta rendendo dal browser al controller.

La guida caricamento di file nella documentazione Phoenix dovrebbe essere utile a voi: http://www.phoenixframework.org/docs/file-uploads

Dalla documentazione:

Una volta che abbiamo la struct Plug.Upload disponibile nel nostro controller, possiamo eseguire qualsiasi operazione su di esso vogliamo. Possiamo verificare che il file esista con File.exists?/1, copiarlo da qualche altra parte sul file system con File.cp/2, inviarlo a S3 con una libreria esterna, o persino inviarlo al client con Plug .Conn.send_file/5.

Penso che quello che sta accadendo nel tuo caso è che il file caricato viene cancellato quando il processo finisce poiché non hai salvato la versione temporanea di esso altrove. (Suppongo che tu non stia già memorizzandolo nel DB.) Scriverò il codice per farlo nel tuo controllore/i dopo aver verificato che il changeset sia valido.

Problemi correlati