Rails 3 + JQuery-File-Upload + Nested Model
Asked Answered
G

3

12

I've been searching for some examples, but have come up short:

I'm trying to implement JQuery-File-Upload on a project I'm working on, but am getting lost as to how to get it to work with nested attributes.

Quick overview:

2 Models:

Comment [has_many :attachments]
Attachment [belongs_to :comment]

Comment accepts_nested_attributes_for :attachments. Also - I'm using Dragonfly.

I've reviewed the Rails 3 guides on the JQuery-File-Upload site, but they assume it's a singular model, so it's all built around a form. Does anyone have any examples of their implementation or is there an existing tutorial that I haven't yet stumbled across?

I'm sure someone has had a similar issue... is JQuery-File-Upload to appropriate tool or should I look at something else?

Garcon answered 20/2, 2012 at 7:32 Comment(2)
Hey, I am having a similar issue where I cannot seem to use JQuery-File-Upload with nested_form gem. Were you able to find a solution to this?Lydie
You're asking for AJAX-upload specifically, as in the file sends while the user is still filling out the rest of the form, yes? Are you working with images or just files in general? And, does the Attachment model have anything in it other than the files -- any validations etc?Deviation
W
6

I just wanted to throw my answer in here as well as Stone's. I spent nearly two solid days getting this to work (Stone was right, it was a PITA!), so hopefully my solution will help someone. I did it just a touch different than Stone.

My app has Features (a comic, puzzle, text-column, etc) and FeatureAssets (individual comic panels/color versions, question & answer files for a specific crossword, etc). Since FeatureAssets are solely related to one Feature, I nested the models (as you'll see in my upload form).

The biggest problem for me was realizing that my params[:feature_asset] that was being sent to the server was actually an array of my uploader'd file objects, instead of just the one I was used to working with. After a bit of fiddling with iterating through each file and creating a FeatureAsset from it, it worked like a charm!

Hopefully I'll translate this clearly. I'd rather provide a bit too much information than not enough. A little extra context never hurts when you're interpreting someone else's code.

feature.rb

class Feature < ActiveRecord::Base
  belongs_to :user
  has_many :feature_assets

  attr_accessible :name, :description, :user_id, :image

  accepts_nested_attributes_for :feature_assets, :allow_destroy => true

  validates :name,    :presence => true
  validates :user_id, :presence => true

  mount_uploader :image, FeatureImageUploader
end

feature_asset.rb

  belongs_to :user
  belongs_to :feature

  attr_accessible :user_id, :feature_id, :file, :file_cache

  validates :user_id,     :presence => true
  validates :feature_id,  :presence => true
  validates :file,        :presence => true

  mount_uploader :file, FeatureAssetContentUploader

  # grabs useful file attributes & sends them as JSON to the jQuery file uploader
  def to_jq_upload
    {
      "file" => file,
      "file_name" => 'asdf',
      "url" => file.url,
      "delete_url" => id,
      "delete_type" => "DELETE"
    }
  end

feature_assets_controller.rb

  def create
    @feature = Feature.find(params[:feature_id])

    params[:feature_asset]['file'].each do |f|
      @feature_asset = FeatureAsset.create!(:file => f, :feature_id => @feature.id, :user_id => current_user.id)
    end

    redirect_to @feature
  end

And not that it probably helps that much, but my feature_asset_uploader.rb is below. It's pretty stripped down.

class FeatureAssetContentUploader < CarrierWave::Uploader::Base

  storage :file

end

features _form.html.erb (similar to Stone's, but not quite)

<%= form_for [@feature, @feature_asset], :html => { :multipart => true  } do |f| %>
  <div class="row" id="fileupload">
    <div class=" fileupload-buttonbar">
      <div class="progressbar fileupload-progressbar nofade"><div style="width:0%;"></div></div>
      <span class="btn btn-primary fileinput-button">
        <i class="icon-plus"></i>
        <span><%= t('feature_assets.add_files') %>...</span>
        <%= hidden_field_tag :feature_id, @feature.id %>
        <%= hidden_field_tag :user_id, current_user.id %>
        <%= f.file_field :file, :multiple => true %>
      </span>
      <button type="submit" class="btn btn-success">Start Upload</button>
      <button type="reset" class="btn btn-warning">Cancel Upload</button>
      <button type="button" class="btn btn-danger">Delete Files</button>
    </div>
  </div>

It doesn't have error handling or any of the niceties it should have, but that's the barebones version of it.

Hopefully that helps someone out there. Feel free to ask me if you have any questions!

Kyle

Whereunto answered 18/6, 2013 at 21:22 Comment(3)
I am trying to do the same think as you and stone but I need feature_asset to be contained in the same form @feature is created. Any pointers?Goebbels
@AlainGoldman - As in you're trying to create the Feature and it's FeatureAssets on the same form? This is the tutorial & app I based my code from. It does exactly that! github.com/tors/jquery-fileupload-railsWhereunto
I solved this by using a solution similar to: railscook.com/recipes/…Incestuous
B
1

I have a similar setup running with Carrierwave. Here's what I have. I'm using Images as a nested resource for Projects.

Project.rb:

has_many :images, :dependent => :destroy
accepts_nested_attributes_for :images, :allow_destroy => true

Image.rb:

 include Rails.application.routes.url_helpers
  mount_uploader :image, ImageUploader

  belongs_to :project

  #one convenient method to pass jq_upload the necessary information
  def to_jq_upload
  {
    "name" => read_attribute(:image),
    "size" => image.size,
    "url" => image.url,
    "thumbnail_url" => image.thumb.url,
    "delete_url" => image_path(:id => id),
    "delete_type" => "DELETE" 
   }
  end

Images_controller.rb:

 def create
    @image = Image.new(params[:image])
    @image.project_id = params[:project_id]
    @project = Project.find(params[:project_id])
    @image.position = @project.images.count + 1
    if @image.save
      render :json => [ @image.to_jq_upload ].to_json
    else
      render :json => [ @image.to_jq_upload.merge({ :error => "custom_failure" }) ].to_json
    end
  end

Be aware, this was a *!@^%! to get working.

UPDATE: projects/_form.html.erb

<div id="fileupload" class="image_add">
    <%= form_for Image.new, :html => {:multipart => true} do |f| %>
        <div class="fileupload-buttonbar">
            <label class="fileinput-button">
                <span>Add files...</span>
                <%= hidden_field_tag :project_id, @project.id %>
                <%= f.file_field :image %>
            </label>
            <button type="submit" class="start">Start Upload</button>
            <button type="reset" class="cancel">Cancel Upload</button>
            <button type="button" class="delete">Delete Files</button>
        </div>
    <% end %>
    <div class="fileupload-content">
        <div class="dropzone-container">
            <div class="dropzone">Drop Image Files Here</div>
        </div>
        <table class="files"></table>
    </div>
</div>
Bailment answered 18/5, 2012 at 22:29 Comment(10)
Hey thanks for the this answer. I'm trying to get the same solution going. Can you post the form view too? Thanks!Ventose
It is a huge *#&*(# to get working. I'm trying to figure out how to structure the form. Nested form doesn't seem to work out... <input id="post_images_attributes_0_post_picture" name="post[images_attributes][0][post_picture][]" type="file" />Ventose
Sure Marc, It's been a long time since I've looked at this code, but here's the form code for Image uploading. I'm just passing the project_id in a hidden_field_tag so I can grab it later. Best of luck!Bailment
PS - I might have rewritten the form code to not be nested now that I look back at it. Anyways, this is what I have live. Hopefully, the extra info is helpful.Bailment
I see. So you aren't creating the project with the images at the same time? I'm trying to submit (using your example) a new project with several images. Any ideas?Ventose
What I'm doing will work for you. It's just not using a nested form. I had it like that originally (nested) but the UI didn't call for it at one point. Here's the railscast that I modeled all my nested forms on. Take a look at Ryan's JS, it's pretty nifty: railscasts.com/episodes/197-nested-model-form-part-2Bailment
Ok. I'll take a closer look. Quickly it looks like there is a hidden field in the form with @project.id yet it hasn't been created yet... So not sure what value that should be pointing to in te formVentose
#12350519Ventose
I am trying to do the same think as you but I need @project to be contained in the same form image is created. Any pointers?Goebbels
@AlainGoldman - definitely look at Ryan Bates's Railscasts about nested forms. You should be able to apply the information from the answers on this page into your scenario. Like I said earlier though, this was a HUGE PITA... Start working on this in the morning, not late at night after a long day :)Bailment
S
0

I have coped with this problem and made a demo app to show how to do this.

In short I have two models: item and upload.

item.rb:

has_many :uploads
accepts_nested_attributes_for :uploads, :allow_destroy => true

upload.rb:

belongs_to :item
    has_attached_file :upload, :styles => { :large => "800x800", :medium => "400x400>", :small => "200x200>" }

I added uploads_attributes to item controller.

Now you can add jquery-file-upload form to your view, but there is one problem: it sends each photo in separate requests. So there is my jquery-file-upload initializer, which uploads all photos in one request (creating item model) and then redirecting to the root of your app (you need to use item form):

<script type="text/javascript" charset="utf-8">
    var num_added = 0;
    var added = 0;
    var all_data = {};
    $(function () {
        // Initialize the jQuery File Upload widget:
        $('#fileupload').fileupload({
          complete: function (e, data) {
            window.location = "<%= root_url %>";
        },
          singleFileUploads: false
        })  .bind('fileuploadadd', function (e, data) {num_added++;})
            .bind('fileuploadsubmit', function (e, data) {
            if(added < num_added)
            {
            if (added == 0)
            all_data = data;
            else
            {
            $.each(data['files'], function(i, file){
            all_data['files'].push(file);
            });
            $.each(data['context'], function(i, context){
            all_data['context'].push(context);
            });
            }
            added++;
            if (added == num_added)
            {
            added++;
            all_data.submit();
            }
            return false;
            }
            })
            .bind('fileuploadsend', function (e, data) {num_added = 0; added = 0;});

        // 
        // Load existing files:
        $.getJSON($('#fileupload').prop('action'), function (files) {
          var fu = $('#fileupload').data('blueimpFileupload'), 
            template;
          fu._adjustMaxNumberOfFiles(-files.length);
          console.log(files);
          template = fu._renderDownload(files)
            .appendTo($('#fileupload .files'));
          // Force reflow:
          fu._reflow = fu._transition && template.length &&
            template[0].offsetWidth;
          template.addClass('in');
          $('#loading').remove();
        });

    });
  </script>
Sager answered 6/10, 2013 at 9:47 Comment(1)
Note that link-only answers are discouraged, SO answers should be the end-point of a search for a solution (vs. yet another stopover of references, which tend to get stale over time). Please consider adding a stand-alone synopsis here, keeping the link as a reference.Toluol

© 2022 - 2024 — McMap. All rights reserved.