`
xf986321
  • 浏览: 163918 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Multiple Attachments in Rails

阅读更多

Multiple Attachments in Rails

the orginal URL:http://www.practicalecommerce.com/blogs/post/432-Multiple-Attachments-in-Rails

 

Creating intuitive interfaces can be challenging, particularly when it comes to uploading files such as images or attachments. Suppose that we are creating an application that let's someone submit a blog post, to which they can add multiple attachments. A great example of this kind of thing is BaseCamp , which is a project management application. BaseCamp is one of those applications that leaves developers in awe, as they have managed to create extremely intuitive interfaces. Inspired by the way that BaseCamp handles multiple file uploads, I set out to create my own version. Here is what I learned.

Objective

My objective was to create an interface that would a user to submit a blog post, with the ability to attach multiple files to that job post. The interface needed to be intuitive, graceful, and (hopefully) on par with the way that BaseCamp does it. To start out, I knew that I would be using Ruby on Rails to create my application, and that I would also be using the attachment_fu plugin to handle file uploads.

A bit of searching about multiple file uploads, and I was ready to get started. Having done a little research and working out the logic of the problem, I figured I would have the following objectives:

  • Allow user to attach files to a blog post.
  • Enforce a limit of 5 files per blog post.
  • Allow a user to remove files from a blog post.

The Models

Let's start with the models that we will need in order to pull this off. Since we are trying to let a user create a blog post, we will start with a model called Post . We'll also need a model that will represent our attached file, which I am going to call Attachment . The following is a sample migration file to create these models:

class CreatePosts < ActiveRecord::Migration
      def self.up
        create_table :posts do |t|
          t.string    :title
          t.text      :content
          t.timestamps
        end
        create_table :attachments do |t|
          t.integer     :size, :height, :width, :parent_id, :attachable_id, :position
          t.string      :content_type, :filename, :thumbnail, :attachable_type
          t.timestamps
          t.timestamps
        end
        add_index :attachments, :parent_id
        add_index :attachments, [:attachable_id, :attachable_type]
      end

      def self.down
        drop_table :posts
            drop_table :attachments
      end
    end

Most of the fields in the attachments table are required by the attachment_fu plugin, but you'll notice that I have added an integer field called attachable_id and a string field called attachable_type . These fields are going to be used for a polymorphic relationship. After all, if this thing works I don't want to be limited to only attaching files to blog posts, but would rather have the option of adding attachments to other models in the future. Additionally, I've added an indexes to the attachments table based on my experiences with the SQL queries that attachment_fu generates. Without going in to detail, these indexes help immensely when your application begins to scale.

So once you have migrated your database, it's time to move on to the actual models themselves. Let's start with the Post model (/app/models/post.rb ):

class Post < ActiveRecord::Base

      has_many  :attachments, :as => :attachable, :dependent => :destroy

      validates_presence_of   :title
      validates_uniqueness_of :title
      validates_presence_of   :content

    end

This is a pretty basic model file. To start with, we declare that this model has_many attachments, which we will reference as "attachable" (remember the extra fields we added to the attachments table?), and that if someone deletes a post, the attached files should also be deleted. Then we do some simple validations to make sure that each post has a unique title and some content.

Moving on, let's take a look at our Attachment model (/app/models/attachment.rb ):

class Attachment < ActiveRecord::Base

      belongs_to :attachable, :polymorphic => true

      has_attachment :storage => :file_system,
                     :path_prefix => 'public/uploads',
                     :max_size => 1.megabyte

    end

Again, this is an extremely basic model file. The first thing we do is set the belongs_to relationship, which is polymorphic. Notice that we are referring to attachments as attachable , like we did in our Post model.

We are going to be storing our uploaded files in /public/uploads . When it comes to deployment, be sure that this directory is symlinked to a shared directory, or you will lose all your attachments each time you deploy.

Now that we have our two models in place, let's set up some controllers and get our views figured out.

Setting Up Controllers

Ok, so what we know is that we will be creating blog posts. First off, let's create some controllers to handle all of this. I'll be coming back to the controller actions later, but for now we want to generate them:

script/generate controller posts
    script/generate controller attachments

I like to create a controller for each resource in order to keep things RESTful. You never know where your app may go, so better safe than sorry. Speaking of resources, let's create some routes as well (/config/routes.rb ):

map.resources :attachments
map.resources :posts

We'll come back to write our controller actions later, as first we want to tackle our view files.

The Basic Views

I'm going to operate under the assumption that we are using a layout template, which loads in all of the default JavaScript files for Rails:

<%= javascript_include_tag :defaults %>

From there, let's start by looking at the basic forms that we are starting with to manage blog posts:

(/app/views/posts/new.html.erb )

    <% form_for(:post, @post, :url => posts_path, :html => {:onsubmit => 'return post.validate();', :multipart => true}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <p>
            <%= f.submit "Create Blog Post", :id => 'post_submit' %>
        </p>
    <% end -%>

(/app/views/posts/edit.html.erb )

    <% form_for(:post, @post, :url => post_path(@post), :html => {:onsubmit => 'return post.validate();', :multipart => true, :method => :put}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <p>
            <%= f.submit "Save Changes", :id => 'post_submit' %>
        </p>
    <% end -%>

(/app/views/posts/_post_form.html.erb )

<p>
      <label for="post_title">Title:</label>
      <%= f.text_field :title %>
    </p>
    <p>
      <label for="post_content">Content:</label>
      <%= f.text_area :content, :rows => 7 %>
    </p>

By using a partial to abstract out the blog post fields, we can concentrate on the other parts of the form. You'll notice that there are no file fields or any uploading stuff at all in our forms. We are going to add this in, but it needs to be one at a time. The reason for this is that we have two very different scenarios that require different logic:

  1. A user is creating a new blog post – under this scenario, there are no attachments to our post, since it is new. All we need to be concerned about is allowing multiple uploads and uploading them to the server.
  2. A user is editing a blog post – things get a little more complicated here. Let's assume that we are limiting the number of attachments to 5 per blog post. If we are editing a post that already has 2 attachments, we need a way to make sure that they cannot go over the limit. Additionally, the user will need a method of removing attachments that are already assigned to the blog post (which also plays in with the attachment limit).

First of all, let's build the JavaScript that we will need to make this happen, and then we will come back to our views and make the adjustments that we need.

The JavaScript

Alright, some of you have probably been reading through this post wondering when I am going to actually start talking about how to upload multiple files. Your patience is about to pay off! The way that this trick works is that we use JavaScript to store the files that a user wants to attach, and also to display an interface. To set the expectation of what we are looking at, here is a timeline of events for uploading multiple files:

  1. A user selects a file to attach.
  2. That file is stored via JavaScript.
  3. The file name is appended to the page with a link to remove the file.
  4. The number of attachments is evaluated, and the file field is either reset or de-activated.

Attached Files As you can see in the screenshot at the left, once a user has selected a file to attach, it is displayed to the user and they have the option of removing it. Note: these files have not been uploaded yet, they are simply stored in a JavaScript object .

First of all, I found a nice little script by someone called Stickman after searching a bit on this. However, I found that I needed to make some small adjustments to the original script and the examples provided, particularly:

  • Changed the uploaded attachments from :params['file_x'] to params[:attachment]['file_x'] .
  • Changed to update a ul element with li elements, rather than creating div elements.
  • Changed to "Remove" button to a text link.
  • Uses Prototype and Scriptaculous .

These sample files are included at the bottom of this post. So let's go through the steps that we need to get this working on our example. The first thing we need to do is add the scripts to /public/javascripts/application.js so that we have the JavaScript methods that we need in place. You will notice that there is also a validation script in there to handle client-side validation of the blog post form (post.validate() ).

The "New Blog Post" Form

Let's take a look at what we need to add to our "new blog post" template:

(/app/views/posts/new.html.erb )

    <% form_for(:post, @post, :url => posts_path, :html => {:onsubmit => 'return post.validate();', :multipart => true}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <% fields_for @attachment do |attachment| -%>
        <p>
            <label for="attachment_data">Attach Files:</label>
            <%= attachment.file_field :data %>
        </p>
        <% end -%>
        <ul id="pending_files"></ul>
        <script type="text/javascript">
            var multi_selector = new MultiSelector($('pending_files'), 5);
            multi_selector.addElement($('attachment_data'));
        </script>
        <p>
            <%= f.submit "Create Blog Post", :id => 'post_submit' %>
        </p>
    <% end -%>

As you can see, we have added some code between our partial (below the blog post fields) and the submit button. Let's take a quick look at what each of these does:

<% fields_for @attachment do |attachment| -%>

This block allows us to create form fields for another model. In our case, the form that we are working with is linked to the Post model, but we would like to upload files to the Attachment model. Inside this block we have a variable called attachment that maps to an Attachment object, similar to the @post variable mapping to a Post object.

<p>
        <label for="attachment_data">Attach Files:</label>
        <%= attachment.file_field :data %>
    </p>

Here we are adding the file field. The script overrides much of the attributes for this one, but just for good form I have called it data. Note how the label corresponds to attachment_data , since that is the ID that Rails will generate for that file field.

<ul id="pending_files"></ul>

Here are have put an empty ul element that will hold the "pending files" that we want to upload. Remember when we select a file for upload, it will be stored and displayed here (with the option to remove it). Notice that this unordered list element has an id of pending_files .

<script type="text/javascript">
        var multi_selector = new MultiSelector($('pending_files'), 5);
        multi_selector.addElement($('attachment_data'));
    </script>

This is the real meat of the script, where we create an instance of the MultiSelector object. It takes two parameters, the first is the id of the element to update after a file has been selected, and the second is an option limit number. In our example, we are putting a limit of 5 attachments per blog post. We could leave this blank to allow unlimited attachments.

Secondly, we add the file field element to our MultiSelector instance, which hooks it all up to the file field that we had created above. And really, that is about all that we need to do.

The "Edit Blog Post" Form

With the edit form, we have a little bit more to deal with. Specifically, because there could already be attachments to the blog post that we want to edit, we have a few issues to face:

  1. We need to evaluate how many new attachments are allowed.
  2. We need to display the attachments that already exist.
  3. A user needs to be able to remove the attachments that already exist.

So let's take a look at the final code for our edit form, and go through what each of those changes is:

(/app/views/posts/edit.html.erb )

    <% form_for(:post, @post, :url => post_path(@post), :html => {:onsubmit => 'return post.validate();', :multipart => true, :method => :put}) do |f| %>
        <%= error_messages_for 'post' %>
        <%= render :partial => 'post_form', :locals => {:f => f, :post => @post} %>
        <% fields_for @newfile do |newfile| -%>
        <p>
            <label for="newfile_data">Attach Files:</label>
            <% if @post.attachments.count >= 5 -%>
                            <input id="newfile_data" type="file" />
            <% else -%>
            <input id="newfile_data" type="file" disabled />
            <% end -%>
        </p>
        <% end -%>
        <ul id="pending_files">
            <% if @post.attachments.size > 0 -%>
            <%= render :partial => "attachment", :collection => @post.attachments %>
            <% end -%>
        </ul>
        <script type="text/javascript">
            var multi_selector = new MultiSelector($('pending_files'), <%= @allowed %>);
            multi_selector.addElement($('newfile_data'));
        </script>
        <p>
            <%= f.submit "Save Changes", :id => 'post_submit' %>
        </p>
    <% end -%>

You'll notice some similarities, but we have to make adjustments here on the edit form. Let's start with the changes that we've made:

<% fields_for @newfile do |newfile| -%>
    <p>
        <label for="newfile_data">Attach Files:</label>
        <input id="newfile_data" type="file" />
        <% if @post.attachments.count >= 5 -%>
        <% else -%>
        <input id="newfile_data" type="file" disabled />
        <% end -%>
    </p>
    <% end -%>

Again we are using a fields_for block here to assign fields to a new model. However, in order to control whether or not the file field is disabled I have hard-coded the file field. The only reason that the fields_for block is there is to keep consistent, and for example purposes. Notice that if the post that we are going to edit already has 5 attachments, the file field is disabled to prevent more attachments.

<ul id="pending_files">
        <% if @post.attachments.size > 0 -%>
        <%= render :partial => "attachment", :collection => @post.attachments %>
        <% end -%>
    </ul>

Once again we have our ul element that displays the attachments. Since the blog post that we are editing may already have attachments, we loop through each attachment that it may already have an display a partial for each one:

(/app/views/posts/_attachment.html.erb )

<%= "<li id=\"attachment_#{attachment.id}\">#{attachment.filename} %>
    <%= link_to_remote "Remove", :url  => attachment_path(:id => attachment), :method => :delete, :html => { :title  => "Remove this attachment" } %></li>

As you can see, all this partial does is display a li element that displays the filename of the attachment and also an Ajax link to remove that attachment. We'll deal with the Ajax link more when we get to our controllers. We still have one more section left that is new in our edit form:

<script type="text/javascript">
        var multi_selector = new MultiSelector($('pending_files'), <%= @allowed %>);
        multi_selector.addElement($('newfile_data'));
    </script>

This is the familiar script that creates our MultiSelector object. However, this time we are using a variable called @allowed to declare the number of attachments that are allowed to be uploaded. Remember that we have a limit of 5 attachments for each blog post, so we need to evaluate how many this particular post already has, and then respond accordingly.

The Controllers

So far we have put everything in place that we need to get multiple file uploads to happen. We've created the models that we need to hold our data, and we've created the views that we need to create and edit our models. Now all we need to do is to tie it all together. Remember that we have two resources, Post and Attachment , that we are working with, so we also have two controllers. Since most of the action happens in the Post controller, let's start with that one:

(/apps/controllers/\posts_controller.rb )

def new
        @post = Post.new
        @attachment = Attachment.new
    end

    def create
        @post = Post.new(params[:post])
        success = @post && @post.save
        if success && @post.errors.empty?
            process_file_uploads
            flash[:notice] = "Blog post created successfully."
            redirect_to(post_path(:id => @post))
        else
            render :action => :new
        end
    end

    def edit
        @post = Post.find(params[:id])
        @newfile = Attachment.new
        @allowed = 5 - @post.attachments.count
    end

    def update
        if @post.update_attributes(params[:post])
            process_file_uploads
            flash[:notice] = "Blog post update successfully."
            redirect_to(post_pat(:id => @post))
        else
            render :action => :edit
        end
    end

    protected

    def process_file_uploads
        i = 0
        while params[:attachment]['file_'+i.to_s] != "" && !params[:attachment]['file_'+i.to_s].nil?
            @attachment = Attachment.new(Hash["uploaded_data" => params[:attachment]['file_'+i.to_s]])
            @post.attachments << @attachment
            i += 1
        end
    end

Obviously, you would want to include the other RESTful actions to your actual controller such as index , show and destroy . However, since these are the only ones that are different for multiple file uploads, they are the only ones that I am showing. You'll notice that we have a nice little action that handles the multiple file uploads. Not a bad 8 lines of code, but basically the create and update actions are expecting the following parameters:

{
    "post" => {"title" => "Blog Post Title",
                            "content" => "Blog post content..."},
    "attachment" => {"file_0" => "FILE#1",
                                        "file_1" => "FILE#2"}
    }

It will then loop through all of the valid attachment files, save them and assign them to the blog post in question. Also notice that we have set the @allowed value in the edit action to make sure that our view knows how many attachments a blog post already has.

At this point we are in good shape. Our forms will work and we can upload multiple attachments. However we still haven't solved one of our problems, which is that we need to be able to remove attachments that were previously assigned to a blog post, which comes up on our edit form. Let's take a look at our other controller and see how we handle this challenge:

(/apps/controllers/\attachments_controller.rb )

def destroy
        @attachment = Attachment.find(params[:id])
        @attachment.destroy
        asset = @attachment.attachable
        @allowed = 5 - asset.attachments.count
    end

So let's take a look at this, which is a pretty basic destroy controller. Remember that we have Ajax links to this action from the attached files on our edit form. Each link sends the id of the attachment to be removed, which is passed to this controller. Once we have deleted the attachment from the database, we then want to get the "asset" that this attachment belongs to. In our example, the "asset" is a blog post, but remember that we made the Attachment relationship polymorphic for a reason. By getting the "asset" that the attachment was assigned to, we can get a count of how many attachments are left, which let's us update our familiar allowed variable.

The only thing left to do is to update the edit form to remove the attachment. Remember that any attachments that are added to the list via the file field are handled by our MultiSelector object, including removing them. However, since we are trying to remove the attachments that were already assigned to our blog post, we'll need to use an rjs template:

(/app/views/attachments/destroy.rjs )

page.hide "attachment_#{@attachments.id.to_s}"
    page.remove "attachments_#{@attachments.id.to_s}"
    page.assign 'multi_selector.max', @allowed
    if @allowed < 5
      page << "if ($('newfile_data').disabled) { $('newfile_data').disabled = false };"
    end

Again this is a pretty simple little file that does a lot. The first thing that we do is to hide the li element for this attachment, and then remove it completely from the DOM just to be sure. In order to update the number of allowed uploads, we assign a new value to multi_selector.max , which is the variable in our script that controls the maximum number of attachments (use -1 for unlimited attachments). Finally, just in case there were 5 attachments before we removed one, which would mean that the file field is disabled, we re-enable that file field if it is appropriate.

And that is about it! Aside from some CSS styling, we now have the ability to upload multiple files and attach them to a blog post using Ruby on Rails. Please download the sample files to see the code in action, and I would love to hear feedback and comments.

 

分享到:
评论

相关推荐

    Outlook add-in to unlock blocked attachments in .NET

    标题中的“Outlook add-in to unlock blocked attachments in .NET”指的是一个使用.NET框架开发的Outlook插件,其主要功能是解锁在Outlook中被阻止的附件。在Outlook中,出于安全考虑,某些类型的附件(如.exe或...

    attachments

    【标题】"attachments"所代表的是一个与软件开发相关的压缩包文件,可能包含了各种开发工具或项目的源码、脚本和配置文件。这个压缩包可能是为了方便开发者在不同环境中快速搭建项目或者进行版本控制。 【描述】...

    django-attachments

    `django-attachments` 是一个专门为 Django 框架设计的附件管理应用,它为用户提供了方便地上传、管理和检索附件的功能。这个应用在 Django 项目中扮演着重要角色,尤其对于那些需要处理大量文件上传和管理的场景,...

    Attachments_2015423.zip_Attachments

    【Attachments_2015423.zip_Attachments】这个压缩包文件包含了与创建参数化曲线相关的代码和图像资源,适用于对计算机图形学、数学建模或MATLAB编程感兴趣的用户。以下是根据提供的文件名解析出的相关知识点: 1. ...

    Laravel开发-laravel-easy-attachments

    `laravel-easy-attachments`是一个专为Laravel设计的包,它简化了这一过程,提供了强大的功能和易用性。这个包的核心目标是帮助开发者更高效地管理应用程序中的附件,无论是单个文件还是多文件,以及图像处理。 ###...

    Attachments

    this is very important file for software developers

    Joomla 插件 attachments 1.3.4

    - `attachments_component.zip`、`attachments_plugin.zip`、`add_attachment_btn_plugin.zip`、`attachments_search.zip`:这些是实际的插件和组件文件,需要通过 Joomla 的后台管理界面进行安装和启用。...

    attachments_Attachments_ad_

    标题中的"attachments_Attachments_ad_"可能是指一系列与附件或数据传输相关的AD(Analog Devices)公司的产品应用,但没有具体说明是哪一种设备或技术。描述中提到的"Design of signal generator based on AD9912"则...

    attachments_Attachments_scientistyfp_doc_

    文档标题“attachments_Attachments_scientistyfp_doc_”暗示了这是一个关于附件处理或科学数据分析的文档,可能涉及性能评估和测试。描述中提到“doc explaining the performance and testing of”进一步证实了这...

    Attachments-freeworld_arqc_arpcgenerator_Attachments_ARQCgenerat

    Attachments-freeworld_arqc_arpcgenerator_Attachments_ARQCgenerat

    attachments(1)_Attachments_android_

    【标题】"attachments(1)_Attachments_android_" 暗示我们关注的是与Android操作系统相关的附件管理或使用场景,可能是关于如何在Android设备上处理不同类型的附件,如文档、图片或其他数据文件。Android系统是一个...

    Attachments from VLC scripting in LabVIEW..._流媒体_网络摄像头_rtsp_labv

    在本主题中,“Attachments from VLC scripting in LabVIEW...”探讨了如何在LabVIEW(Laboratory Virtual Instrument Engineering Workbench)环境中,通过ActiveX接口调用VLC(VideoLAN Client)的DLL(动态链接库...

    Attachments Viewer Component

    Power Platform Power Apps画布应用查看附件组件

    joomla附件插件attachments-3.2.3.zip

    Attachments 插件是专门为 Joomla 设计的一个功能,旨在增强其内置的文件管理能力。Attachments 3.2.3 版本是一个更新,提供了对上传和管理网站内容中附加文件的支持。 Attachments 插件的核心功能包括: 1. **...

    attachments.jar

    attachments.jar

    Attachments_201429.zip_Attachments_cyclostationary_spectrum sens

    research papers of spectrum sensing in cognitive based on cyclostationary detction

Global site tag (gtag.js) - Google Analytics