- 浏览: 163907 次
- 性别:
- 来自: 杭州
文章分类
最新评论
-
GunChin:
有些复杂,看得不是很懂
RAILS -
shellfish:
恩,红帽默认的SELinux的级别是强制,这个一般我不大用,装 ...
华思服务器一个奇怪问题的解决方法 -
机器人:
你说得太好了了了了了了了 子 啊啊啊啊,呼啦啦。
GIT handbook -
hbxiao135:
能介绍下 fat free crm的 流程分析吗?
(CRM)customer relationship management sysetm
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:
- 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.
- 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:
- A user selects a file to attach.
- That file is stored via JavaScript.
- The file name is appended to the page with a link to remove the file.
- The number of attachments is evaluated, and the file field is either reset or de-activated.
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']
toparams[:attachment]['file_x']
. - Changed to update a
ul
element withli
elements, rather than creatingdiv
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:
- We need to evaluate how many new attachments are allowed.
- We need to display the attachments that already exist.
- 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.
发表评论
-
actionmailer发送邮件失败的问题解决记录
2011-05-24 14:12 2334我们公司的WLAN网管采用ruby on rails架构,同时 ... -
ROR rake
2009-10-20 14:05 1046原文: Ruby on Rails Rake Tuto ... -
控制器内部对请求的操作
2009-10-20 13:41 1843控制器内部对请求的操作 一Action方法 1 ... -
在rails环境中直接执行sql语句而不需要创建MODEL
2009-09-23 10:54 2987标准格式是:ActiveRecord::Base.connec ... -
6 Steps To Refactoring Rails (for Mere Mortals)
2009-07-31 11:26 866Since December, Rails ... -
rails 中简单的创建保存一条信息
2009-07-20 09:43 716Securitylog.new( # :ses ... -
uninstall all gems
2009-07-14 11:31 875GEMS=`gem list --no-versions` ... -
rails 拖拉排序效果demo
2009-06-30 10:55 1353要為Ruby on Rails程式加入拖曳排 ... -
something about with_scope
2009-06-23 10:53 1159今天看了一点关于with_scope的知识,有点感觉,写点东西 ... -
Add jQuery datagrids to your Rails applications
2009-06-03 10:39 1521Add jQuery datagrids to your Ra ... -
REST on Rails之自定义路由
2009-06-03 10:03 1685REST on Rails之自定义路由 from L ... -
自定义will_paginage输出
2009-06-03 09:55 786自定义will_paginage输出 from Le ... -
Rails Cookie测试
2009-06-03 09:48 1077Rails Cookie测试 from Le ... -
配置ActionMailer使用GMail发送邮件
2009-06-03 09:45 1451配置ActionMailer使用GMail发送邮件 ... -
rails 记住跳转前的url
2009-06-02 13:44 1381def store_location ses ... -
给Non-ActiveRecord Objects进行validate
2009-06-02 09:33 886对于非ActiveRecord对象的Validation,我们 ... -
file_column简单的照片上传
2009-04-21 18:19 9661.file_column git clone git:/ ... -
cookie + js动态修改iframe 父窗口的链接参数
2009-04-21 18:12 2870取得 由于最近自己做的项目中采用了目前较为流行的经典左右结构 ... -
git 冲突解决
2009-04-21 17:57 5169比较笨的办法就是直接改本地的代码 先git pull ,他 ... -
git 下gw来查看团队的修改
2009-04-07 15:46 856首先开启一个命令行,在命令行中输入 alias gw= ...
相关推荐
标题中的“Outlook add-in to unlock blocked attachments in .NET”指的是一个使用.NET框架开发的Outlook插件,其主要功能是解锁在Outlook中被阻止的附件。在Outlook中,出于安全考虑,某些类型的附件(如.exe或...
【标题】"attachments"所代表的是一个与软件开发相关的压缩包文件,可能包含了各种开发工具或项目的源码、脚本和配置文件。这个压缩包可能是为了方便开发者在不同环境中快速搭建项目或者进行版本控制。 【描述】...
`django-attachments` 是一个专门为 Django 框架设计的附件管理应用,它为用户提供了方便地上传、管理和检索附件的功能。这个应用在 Django 项目中扮演着重要角色,尤其对于那些需要处理大量文件上传和管理的场景,...
【Attachments_2015423.zip_Attachments】这个压缩包文件包含了与创建参数化曲线相关的代码和图像资源,适用于对计算机图形学、数学建模或MATLAB编程感兴趣的用户。以下是根据提供的文件名解析出的相关知识点: 1. ...
`laravel-easy-attachments`是一个专为Laravel设计的包,它简化了这一过程,提供了强大的功能和易用性。这个包的核心目标是帮助开发者更高效地管理应用程序中的附件,无论是单个文件还是多文件,以及图像处理。 ###...
this is very important file for software developers
- `attachments_component.zip`、`attachments_plugin.zip`、`add_attachment_btn_plugin.zip`、`attachments_search.zip`:这些是实际的插件和组件文件,需要通过 Joomla 的后台管理界面进行安装和启用。...
标题中的"attachments_Attachments_ad_"可能是指一系列与附件或数据传输相关的AD(Analog Devices)公司的产品应用,但没有具体说明是哪一种设备或技术。描述中提到的"Design of signal generator based on AD9912"则...
文档标题“attachments_Attachments_scientistyfp_doc_”暗示了这是一个关于附件处理或科学数据分析的文档,可能涉及性能评估和测试。描述中提到“doc explaining the performance and testing of”进一步证实了这...
Attachments-freeworld_arqc_arpcgenerator_Attachments_ARQCgenerat
【标题】"attachments(1)_Attachments_android_" 暗示我们关注的是与Android操作系统相关的附件管理或使用场景,可能是关于如何在Android设备上处理不同类型的附件,如文档、图片或其他数据文件。Android系统是一个...
在本主题中,“Attachments from VLC scripting in LabVIEW...”探讨了如何在LabVIEW(Laboratory Virtual Instrument Engineering Workbench)环境中,通过ActiveX接口调用VLC(VideoLAN Client)的DLL(动态链接库...
Power Platform Power Apps画布应用查看附件组件
Attachments 插件是专门为 Joomla 设计的一个功能,旨在增强其内置的文件管理能力。Attachments 3.2.3 版本是一个更新,提供了对上传和管理网站内容中附加文件的支持。 Attachments 插件的核心功能包括: 1. **...
attachments.jar
research papers of spectrum sensing in cognitive based on cyclostationary detction