Commit e33c6d0c authored by Iasmin Mendes's avatar Iasmin Mendes 💃🏻 Committed by Rodrigo Souto

Image crop

parent 4dfe8cea
......@@ -204,17 +204,17 @@ class CmsController < MyProfileController
record_coming
if request.post? && params[:uploaded_files]
params[:uploaded_files].each do |file|
unless file == ''
@uploaded_files << UploadedFile.create(
{
:uploaded_data => file,
:profile => profile,
:parent => @parent,
:last_changed_by => user,
:author => user,
},
:without_protection => true
)
file = file[1]
if file && file.has_key?('file') && file[:file] != ''
data = {
:uploaded_data => file[:file],
:profile => profile,
:parent => @parent,
:last_changed_by => user,
:author => user,
}
data = data.merge(cropped_params(file)) if file.key?(:crop_x)
@uploaded_files << UploadedFile.create(data, :without_protection => true)
end
end
@errors = @uploaded_files.select { |f| f.errors.any? }
......@@ -374,7 +374,12 @@ class CmsController < MyProfileController
parent = check_parent(params[:parent_id])
if request.post?
begin
@file = UploadedFile.create!(:uploaded_data => params[:file], :profile => profile, :parent => parent) unless params[:file] == ''
file = params[:file].present? ? params[:file] : params[:crop][:file]
data = { :uploaded_data => file,
:profile => profile,
:parent => parent }
data = data.merge(cropped_params(params[:crop])) if params.include?(:crop)
@file = UploadedFile.create!(data) unless params[:file] == ''
@file = FilePresenter.for(@file)
respond_to do |format|
format.js
......@@ -539,4 +544,17 @@ class CmsController < MyProfileController
redirect_to :action => 'new', type: "UploadedFile", back_to: params[:back_to], parent_id: params[:parent_id]
end
end
def cropped_params(file_params)
data = {}
if file_params[:crop_x].present?
data = {
:crop_x => file_params[:crop_x],
:crop_y => file_params[:crop_y],
:crop_w => file_params[:crop_w],
:crop_h => file_params[:crop_h]
}
end
data
end
end
......@@ -447,10 +447,10 @@ module ApplicationHelper
javascript_include_tag script if script
end
def file_field_or_thumbnail(label, image, for_attr, removable = true)
def file_field_or_thumbnail(label, image, for_attr, type = 'default', removable = true)
display_form_field label, (
render :partial => (image && image.valid? ? 'shared/show_thumbnail' : 'shared/change_image'),
:locals => { :image => image, :removable => removable, :for_attr => for_attr }
:locals => { :image => image, :removable => removable, :for_attr => for_attr, :type => type }
)
end
......
module CroppedImageHelper
PREVIEW_IMAGE_MEASURES = {
'default' => { :width => '0', :height => '0' },
'profile' => { :width => '100', :height => '100' },
'blog' => { :width => '178', :height => '34' },
'article' => { :width => '100', :height => '100' },
}
end
......@@ -45,8 +45,8 @@ module ProfileImageHelper
def profile_icon( profile, size=:portrait, return_mimetype=false )
filename, mimetype = '', 'image/png'
if profile.image
filename = profile.image.public_filename( size )
if profile.image.present?
filename = profile.image.public_filename(size)
mimetype = profile.image.content_type
else
icon =
......
module CroppedImage
extend ActiveSupport::Concern
included do
attr_accessible :crop_x, :crop_y, :crop_w, :crop_h
attr_accessor :crop_x, :crop_y, :crop_w, :crop_h
before_save :load_image_and_crop
protected
def load_image_and_crop
if %w(.jpeg .jpg .png .gif .bmp).include? File.extname(temp_path)
image = Magick::ImageList.new(temp_path)
crop(image)
end
end
def crop(image)
if image && self.crop_x.present?
x = crop_x.to_i
y = crop_y.to_i
w = crop_w.to_i
h = crop_h.to_i
image.crop!(x, y, w, h)
image.write(temp_path)
self.crop_x = nil
end
end
end
end
class Image < ApplicationRecord
include UploadSanitizer
include CroppedImage
attr_accessible :uploaded_data, :label, :remove_image
attr_accessor :remove_image
belongs_to :owner, polymorphic: true
......@@ -37,4 +39,10 @@ class Image < ApplicationRecord
File.file?(full_filename(size)) ? File.read(full_filename(size)) : nil
end
protected
def resize_image(img, size)
crop(img)
super
end
end
......@@ -258,7 +258,6 @@ class Person < Profile
FIELDS = %w[
description
image
preferred_domain
nickname
sex
......
......@@ -8,6 +8,7 @@ require 'sdbm' unless RUBY_ENGINE == 'jruby'
class UploadedFile < Article
include UploadSanitizer
include CroppedImage
attr_accessible :uploaded_data, :title
......@@ -42,6 +43,7 @@ class UploadedFile < Article
def title
if self.name.present? then self.name else self.filename end
end
def title= value
self.name = value
end
......@@ -86,7 +88,11 @@ class UploadedFile < Article
# :min_size => 2.megabytes
# :max_size => 5.megabytes
has_attachment :storage => :file_system,
:thumbnails => { :icon => [24,24], :bigicon => [50,50], :thumb => '130x130>', :slideshow => '320x240>', :display => '640X480>' },
:thumbnails => { :icon => [24,24],
:bigicon => [50,50],
:thumb => '130x130>',
:slideshow => '320x240>',
:display => '640X480>' },
:thumbnail_class => Thumbnail,
:max_size => self.max_size,
processor: 'Rmagick'
......
......@@ -91,6 +91,11 @@
<%= labelled_form_field(_('Full name'), text_field(:profile_data, :name)) %>
</div>
<div id='signup-avatar'>
<label class='formlabel'><%= _('Profile Image')%></label>
<%= render :partial => 'shared/change_image', :locals => { :for_attr => 'profile_data[image_builder]', :type => 'profile' } %>
</div>
</div>
<div id="signup-form-profile">
......
......@@ -54,8 +54,7 @@
<%= labelled_form_field(_('Description:'), text_area(:article, :body, :rows => 10, :class => current_editor)) %>
<div id="blog-image-builder">
<%= file_field_or_thumbnail(_('Cover image:'), @article.image, 'article[image_builder]')%>
<%= _("Max size: %s (.jpg, .gif, .png)").html_safe % Image.max_size.to_humanreadable %>
<%= file_field_or_thumbnail(_('Cover image:'), @article.image, 'article[image_builder]', 'blog')%>
</div>
<%= labelled_form_field(_('How to display posts:'), f.select(:visualization_format, [
......
......@@ -21,8 +21,11 @@
"type='Folder' or type='Gallery'"
) %>
<%= button(:newfolder, _('New folder'), '#', :id => 'new-folder-button') %>
<%= render partial: 'shared/crop_button' %>
</div>
<p><%= file_field_tag('file', :multiple => true) %></p>
<p>
<%= file_field_tag('file', :multiple => true) %>
</p>
<% end %>
</div>
<div class='hide-and-show-uploads'>
......
<p><%= file_field_tag('uploaded_files[]', id: :file_upload, size: size, multiple: true) %></p>
<% name = name ? name : 'uploaded_files[]' %>
<% single_file = defined?(single_file) ? single_file : false %>
<% file_name = single_file ? "#{name}[uploaded_data]" : "#{name}[file]" %>
<div class='file-fieldset template'>
<div class='file-preview'>
<%= render :partial => 'shared/cropped_image_preview', :locals => { :image => nil, :type => 'default' } %>
</div>
<div class='file-name'></div>
<div class='file-size'></div>
<div class='file-actions'>
<%= button_to_function :trash, '', 'remove_uploaded_file(this)' %>
</div>
<div class='file-inputs'>
<%= file_field_tag(file_name,
:size => size,
:class => 'picture-input',
:onChange => 'checkImageToCrop(this)') %>
<%= render :partial => 'shared/crop_image_form',
:locals => { :for_attr => name, :single_file => single_file} %>
</div>
</div>
<%= safe_join(@plugins.dispatch(:upload_files_extra_fields, params[:parent_id]).collect { |content| instance_exec(&content) }, "") %>
<%= form_for('uploaded_file', :url => { :action => 'upload_files', controller: 'cms' },
:html => {:multipart => true}) do |f| %>
<% if parent %>
<%= hidden_field_tag('parent_id', parent.id) %>
<% else %>
<%= select_profile_folder(_('Choose folder to upload files:'), :parent_id, profile) %>
<% end %>
<%= safe_join(@plugins.dispatch(:upload_files_extra_fields, params[:parent_id]).collect { |content| instance_exec(&content) }, "") %>
<% if parent %>
<%= hidden_field_tag('parent_id', parent.id) %>
<% else %>
<%= select_profile_folder(_('Choose folder to upload files:'), :parent_id, profile) %>
<% end %>
<div id='uploaded_files'>
<%= render :partial => 'cms/upload_file', locals: { size: size } %>
</div>
<div id='uploaded_files'></div>
<%= hidden_field_tag('back_to', back_to) %>
<%= hidden_field_tag('back_to', back_to) %>
<%= button_bar do %>
<%= button_to_function :plus, _('Add files...'), "add_new_files()", class: "button with-text btn-green" %>
<% if defined?(back_to) && back_to.present? %>
<%= submit_button :upload, _('Upload'), :cancel => back_to %>
<% else %>
<%= submit_button :upload, _('Upload'), :cancel => {:action => (@parent ? 'view' : 'index'), :id => @parent } %>
<%= button_bar do %>
<%= button_to_function :plus, _('Add files...'), "add_uploaded_file()", class: "button with-text btn-green" %>
<% if defined?(back_to) && back_to.present? %>
<%= submit_button :upload, _('Upload'), :cancel => back_to %>
<% else %>
<%= submit_button :upload, _('Upload'), :cancel => {:action => (@parent ? 'view' : 'index'), :id => @parent } %>
<% end %>
<% end %>
<%= modal_inline_link_to '', '#', '#crop-image', id: "crop-image-button", style: "display: none" %>
<%= render 'shared/crop_image' %>
<% end %>
<table class="uploaded_files_table">
<tbody>
</tbody>
</table>
<%= render partial: 'cms/upload_file',
locals: { name: "uploaded_files[]",
size: size } %>
<%= labelled_form_field(_('Title'), text_field(:article, :title, :maxlength => 60)) %>
<%= render :partial => 'upload_file', :locals => {:size => '45', :name => 'article[uploaded_data]' } %>
<%= render :partial => 'upload_file', :locals => {:size => '45', :name => 'article', :single_file => true} %>
<%= render :partial => 'general_fields' %>
......@@ -8,3 +8,8 @@
<% if @article.image? %>
<%= f.text_field(:external_link, :size => 64) %>
<% end %>
<%= modal_inline_link_to '', '#', '#crop-image', id: "crop-image-button", style: "display: none" %>
<%= render 'shared/crop_image' %>
<%= javascript_include_tag 'cropped_image' %>
......@@ -21,9 +21,8 @@
<h5><%= (_('Uploading files to %s') % content_tag('code', @target)).html_safe%></h5>
<%= form_for('uploaded_file', url: { action: 'upload_files', controller: 'cms' }, html: { multipart: true, id: :file_upload_form }) do |f| %>
<%= render :partial => 'upload_file_form',
:locals => { :size => '45', back_to: @back_to, parent: @parent,
:num_of_files => 3 } %>
<%= render :partial => 'upload_file_form',
:locals => { :size => '45', back_to: @back_to, parent: @parent } %>
<% end %>
<%= javascript_include_tag 'cropped_image' %>
......@@ -18,10 +18,8 @@
<h2><%= _('Change picture') %></h2>
<span><%= unchangeable_privacy_field @profile %></span>
</div>
<div id="profile_change_picture">
<%= file_field_or_thumbnail(_('Image:'), @profile.image, 'profile_data[image_builder]') %>
<%= _("Max size: %s (.jpg, .gif, .png)").html_safe % Image.max_size.to_humanreadable %>
</div>
<%= file_field_or_thumbnail(_('Image:'), @profile.image, 'profile_data[image_builder]', 'profile') %>
<hr>
......
......@@ -2,7 +2,7 @@
<% if image.is_a? UploadedFile and image.filename %>
<% extension = image.extension %>
<% if ['jpg', 'jpeg', 'gif', 'png', 'tiff', 'svg'].include? extension %>
<% if ['jpg', 'jpeg', 'gif', 'png', 'svg'].include? extension %>
<%= link_to image_tag(image.public_filename(:thumb)), image.view_url, class: 'search-image-pic' %>
<% elsif ['pdf'].include? extension %>
<%= link_to '', image.view_url, :class => 'search-image-pic icon-application-pdf' %>
......
<%= file_field_tag("#{for_attr}[uploaded_data]", { onchange: 'update_image(this)', style: 'display: none;' } ) %>
<%= button_to_function :plus, _('Add image'), "add_new_image(this); return false;", class: "button with-text icon-plus" %>
<%= content_tag :span, '', id: 'img_name' %>
<%= labelled_form_field(_("Image Label:"), text_field_tag("#{for_attr}[label]")) %>
<%= button_to_function(:cancel, _('Cancel'),"jQuery('#change-image-link').show(); jQuery('#change-image').hide()", :id => 'cancel-change-image-link', style: 'display: none')%>
<div class='file-fieldset'>
<div id='change-image' style='<%= "display:#{display}" if display %>' >
<%= button_to_function :plus, _('Add image'), "add_new_image(this)",
class: "button with-text icon-plus" %>
<%= file_field_tag("#{for_attr}[uploaded_data]",
{ :onchange => 'cropImage(this)',
:accept => ".jpg, .gif, .png, .bmp",
:class => "picture-input" } ) %>
<div><%= _("Max size: %s (.jpg, .gif, .png, .bmp, .tiff)").html_safe % Image.max_size.to_humanreadable %></div>
<%= labelled_form_field(_("Image Label:"), text_field_tag("#{for_attr}[label]")) %>
<%= button_to_function(:cancel,_('Cancel'), 'hideen_change_image()',
:id => 'cancel-change-image-link',
:style => 'display: none') %>
<%= render :partial => 'shared/crop_image_form', :locals => { :for_attr => for_attr } %>
<%= modal_inline_link_to '', '#', '#crop-image', id: "crop-image-button", style: "display: none" %>
<%= render 'shared/crop_image' %>
</div>
<%= render :partial => 'shared/cropped_image_preview',
:locals => { :type => type } %>
</div>
<%= javascript_include_tag 'cropped_image' %>
<%= button(:image, _('Add image'), '#', :id => 'add-cropped-image') %>
<div class='file-fieldset'>
<%= file_field_tag('crop[file]',
:class => 'picture-input',
:accept => "image/*",
:onChange => 'cropImage(this)') %>
<%= render :partial => 'shared/cropped_image_preview', :locals => { :image => nil, :type => 'default' } %>
<%= render :partial => 'shared/crop_image_form',
:locals => { :for_attr => 'crop'} %>
</div>
<div id="crop-image">
<h2>Crop your image:</h2>
<img id="cropbox" />
<div class='action-bar'>
<%= button(:save, _('Crop'), '#', :id => 'confirm-crop-image') %>
</div>
</div>
<% %w[x y w h].each do |attribute| %>
<%= hidden_field_tag "#{for_attr}[crop_#{attribute}]", nil, :class => "crop_#{attribute}"%>
<% end %>
<% image_measures = CroppedImageHelper::PREVIEW_IMAGE_MEASURES[type] %>
<div class='preview-image'
data-width='<%= image_measures[:width] %>'
data-height='<%= image_measures[:height] %>'
style='display:none'>
</div>
......@@ -5,10 +5,10 @@
<% body_method ||= :body %>
<% editor_type = current_editor %>
<% lead_id ||= 0%>
<% f ||= false%>
<% f ||= false %>
<% if @article %>
<%= file_field_or_thumbnail(_('Image:'), @article.image, 'article[image_builder]') %>
<%= file_field_or_thumbnail(_('Image:'), @article.image, 'article[image_builder]', 'article') %>
<% end %>
<%= button :add, _("Add Lead"), '#', :class => "lead-button", :article_id => "#article-lead-"+lead_id.to_s, :style => "margin-left: 0px;" %>
......
<%= image_tag(image.public_filename(:thumb)) %>
<div id='preview-text' data-label="<%= _('Preview') %>"></div>
<div id='preview-image'></div>
<br/>
<div>
<div id='actual-image'><%= image_tag(image.public_filename(:thumb)) %></div>
<%= render :partial => 'shared/change_image', :locals => { :for_attr => for_attr, :display => 'none', :type => type } %>
</div>
<%= button_to_function(:photos, _('Change image'), 'display_change_image()', :id => 'change-image-link' ) %>
<script>
function display_change_image() {
jQuery('#change-image').show();
jQuery('#change-image-link').hide();
jQuery('#cancel-change-image-link').show();
}
</script>
<br/>
<div id='change-image' style='display:none'>
<%= render :partial => 'shared/change_image', :locals => { :image => image, :for_attr => for_attr } %>
</div>
<br/>
<% if image.present? && removable %>
<div id='image-builder-remove-checkbox'>
<%= labelled_form_field(_('Remove image'), check_box_tag("#{for_attr}[remove_image]", true, false))%>
......
class AddCroppedImageToProfiles < ActiveRecord::Migration
def change
add_column :profiles, :cropped_image, :string
end
end
class RemoveImageInCustomPersonFields < ActiveRecord::Migration
def change
Environment.all.each do |env|
env.custom_person_fields.delete(:image)
env.save
end
end
end
......@@ -132,7 +132,8 @@ Feature: blog
| joaosilva | My Blog |
And I go to joaosilva's control panel
And I follow "Configure blog"
And I attach the file "public/images/rails.png" to "Cover image:"
And I attach the file "public/images/rails.png" to "article[image_builder][uploaded_data]"
And I follow "Crop"
And I follow "Save"
When I am on /joaosilva/my-blog
Then there should be a div with class "blog-cover"
......@@ -80,6 +80,7 @@ Background:
And I follow "article-options" within "tr[title='JSilva blog']"
And I follow "Edit" within ".noosfero-dropdown-menu"
And I attach the file "public/images/rails.png" to "article[image_builder][uploaded_data]"
And I follow "Crop"
And I follow "Save"
When I go to joaosilva's control panel
And I follow "Edit sideboxes"
......
......@@ -31,14 +31,17 @@ class CmsControllerTest < ActionController::TestCase
should 'redirect to Work Assignment view page after upload submission' do
@organization.add_member(@person)
work_assignment = create_work_assignment('Work Assignment', @organization, nil, nil)
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')] , :back_to => @work_assignment.url
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain')}},
:back_to => @work_assignment.url
assert_redirected_to work_assignment.url
end
should 'upload submission and automatically move it to the author folder' do
work_assignment = create_work_assignment('Work Assignment', @organization, nil, nil)
@organization.add_member(@person)
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')]
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain')}}
submission = UploadedFile.last
assert_equal work_assignment.find_or_create_author_folder(@person), submission.parent
end
......@@ -53,14 +56,16 @@ class CmsControllerTest < ActionController::TestCase
@organization.add_member(@person)
work_assignment = create_work_assignment('Work Assignment', @organization, true, nil)
assert_equal true, work_assignment.publish_submissions
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')]
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain')}}
submission = UploadedFile.last
assert_equal work_assignment.publish_submissions, submission.published
assert_equal work_assignment.publish_submissions, submission.parent.published
other_work_assignment = create_work_assignment('Other Work Assigment', @organization, false, nil)
assert_equal false, other_work_assignment.publish_submissions
post :upload_files, :profile => @organization.identifier, :parent_id => other_work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')]
post :upload_files, :profile => @organization.identifier, :parent_id => other_work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain')}}
submission = UploadedFile.last
assert_equal other_work_assignment.publish_submissions, submission.published
assert_equal other_work_assignment.publish_submissions, submission.parent.published
......@@ -72,7 +77,8 @@ class CmsControllerTest < ActionController::TestCase
assert !work_assignment.publish_submissions
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')]
post :upload_files, :profile => @organization.identifier, :parent_id => work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain') }}
submission = UploadedFile.last
assert !submission.show_to_followers?
......@@ -83,7 +89,8 @@ class CmsControllerTest < ActionController::TestCase
assert_equal true, other_work_assignment.publish_submissions
post :upload_files, :profile => @organization.identifier, :parent_id => other_work_assignment.id, :uploaded_files => [fixture_file_upload('/files/test.txt', 'text/plain')]
post :upload_files, :profile => @organization.identifier, :parent_id => other_work_assignment.id,
:uploaded_files => { "0" => { :file => fixture_file_upload('/files/test.txt', 'text/plain')}}
submission = UploadedFile.last
assert submission.show_to_followers?
......
......@@ -6,6 +6,7 @@
@import 'stylesheets/catalog';
@import 'stylesheets/profile-editor';
@import 'stylesheets/profile';
@import 'stylesheets/cropped_image';
@import 'stylesheets/content';
......
......@@ -252,3 +252,11 @@
#signup-form-profile {
margin-bottom: 10px;
}
#signup-form #signup-avatar {
margin: 10px auto;
text-align: left;
input { padding-left: 0 }
.file-fieldset { padding: 0 }
.formfieldline { display: none }
}
......@@ -53,10 +53,6 @@
line-height: 15px;
}
#uploaded_files {
display: none;
}
form#file_upload_form .formfieldline label {
display: inline-block;
margin-right: 5px;
......@@ -164,3 +160,14 @@ a.remove-file {
#search-button{
height: 50% !important;
}
#crop-image {
display: none;
width: 700px;
height: 540px;
padding: 10px 8px;
}
#crop-image #cropbox {
margin: auto;
}
......@@ -16,6 +16,7 @@
*= require vendor/jquery.tokeninput.js
*= require vendor/jquery.typewatch.js
*= require vendor/jquery-timepicker-addon/dist/jquery-ui-timepicker-addon.js
*= require vendor/jquery.Jcrop.js
*= require vendor/inputosaurus.js
*= require vendor/reflection.js
*= require vendor/rails.js
......@@ -1154,25 +1155,30 @@ function stop_fetching(element){
jQuery('.fetching-overlay', element).remove();
}
function add_new_file_field() {
var cloned = jQuery('#uploaded_files p:last').clone();
cloned.find("input[type='file']").val('');
cloned.appendTo('#uploaded_files');
function add_uploaded_file() {
var template = $('.file-fieldset.template').clone();
var input = template.find('input[type=file]')[0]
var file_number = $('#uploaded_files .file-fieldset').length
template.removeClass('template')
$(input).attr('name', 'uploaded_files[' + file_number + '][file]')
template.find('.crop_x').attr('name', 'uploaded_files[' + file_number + '][crop_x]')
template.find('.crop_y').attr('name', 'uploaded_files[' + file_number + '][crop_y]')
template.find('.crop_w').attr('name', 'uploaded_files[' + file_number + '][crop_w]')
template.find('.crop_h').attr('name', 'uploaded_files[' + file_number + '][crop_h]')
template.appendTo('#uploaded_files');
$(input).click()
$(input).change(function() {
let file = input.files[0];
template.find('.file-name').text(file.name)
template.find('.file-size').text(format_bytes(file.size))
})
}
function add_new_file_handler() {
$("#uploaded_files p:last input[type='file']").change(function() {
let input = $(this).get(0);
for(let i = 0; i < input.files.length; ++i) {
let file = input.files[i]; file.should_upload = true;
let remove_link = $("<a class='remove-file'><i class='fa fa-trash-o' aria-hidden='true'></i></a>");
let file_row = $("<tr></tr>").append($("<td>" + file.name + "</td>"))
.append($("<td>" + format_bytes(file.size) + "</td>"))
.append($("<td></td>").append(remove_link));
remove_link.click(function() { file.should_upload = false; file_row.fadeOut(500, function() { file_row.remove(); }); });
$("table.uploaded_files_table tbody").prepend(file_row);