Merge pull request #1905 from lrbalt/add-attachments

Add attachments to Todo model using Paperclip
This commit is contained in:
Matt Rogers 2015-08-07 19:49:35 -04:00
commit 00143ece20
19 changed files with 229 additions and 52 deletions

1
.gitignore vendored
View file

@ -15,6 +15,7 @@
/db/*.db
/db/*.sqlite3
/db/*.sqlite3-journal
/db/assets/*
/log/*.log
/public/assets/
/tmp

View file

@ -32,6 +32,7 @@ gem "htmlentities"
gem "swf_fu"
gem "rails_autolink"
gem 'thin'
gem 'paperclip'
# To use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

View file

@ -52,6 +52,10 @@ GEM
xpath (~> 2.0)
childprocess (0.5.5)
ffi (~> 1.0, >= 1.0.11)
climate_control (0.0.3)
activesupport (>= 3.0)
cocaine (0.5.7)
climate_control (>= 0.0.3, < 1.0)
codeclimate-test-reporter (0.4.1)
simplecov (>= 0.7.1, < 1.0.0)
coffee-rails (4.1.0)
@ -101,6 +105,7 @@ GEM
mime-types (>= 1.16, < 3)
metaclass (0.0.4)
mime-types (2.6.1)
mimemagic (0.3.0)
mini_portile (0.6.1)
minitest (5.7.0)
mocha (1.1.0)
@ -112,6 +117,12 @@ GEM
mini_portile (~> 0.6.0)
nokogumbo (1.1.12)
nokogiri
paperclip (4.3.0)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
cocaine (~> 0.5.5)
mime-types
mimemagic (= 0.3.0)
rack (1.5.5)
rack-dev-mark (0.7.3)
rack (>= 1.1)
@ -228,6 +239,7 @@ DEPENDENCIES
jquery-rails
mocha
mysql2
paperclip
rack-dev-mark
rack-mini-profiler
rails (~> 4.1.11)

View file

@ -124,6 +124,11 @@ img.delete_item {
}
}
a.todo_attachment {
background: image-url('bottom_off.png') no-repeat top;
border: none;
}
a.undecorated_link {background-color:transparent;color:transparent;}
img.todo_star {background-image: image-url('staricons.png'); background-repeat: no-repeat; border:none; background-position: -32px 0px;}
img.todo_star.starred{ background-position: 0px 0px; }

View file

@ -815,6 +815,20 @@ class TodosController < ApplicationController
end
end
def attachment
id = params[:id]
filename = params[:filename]
attachment = current_user.attachments.find(id)
if attachment
send_file(attachment.file.path,
disposition: 'attachment',
type: 'message/rfc822')
else
head :not_found
end
end
private
def set_group_view_by

View file

@ -204,6 +204,14 @@ module TodosHelper
link_to(t('todos.convert_to_project'), url, {:class => "icon_item_to_project", :id => dom_id(todo, "to_project")})
end
def attachment_image(todo)
link_to(
image_tag('blank.png', width: 16, height: 16, border:0),
todo.attachments.first.file.url,
{:class => 'todo_attachment', title: 'Get attachments of this todo'}
)
end
def collapsed_notes_image(todo)
link = link_to(
image_tag( 'blank.png', :width=>'16', :height=>'16', :border=>'0' ),

20
app/models/attachment.rb Normal file
View file

@ -0,0 +1,20 @@
class Attachment < ActiveRecord::Base
belongs_to :todo, touch: true
has_attached_file :file,
url: '/:class/:id/:basename.:extension',
path: ":rails_root/db/assets/#{Rails.env}/:class/:id/:basename.:extension",
override_file_permissions: 0660
do_not_validate_attachment_file_type :file
# validates_attachment_content_type :file, :content_type => ["text/plain"]
before_destroy :delete_attached_file
private
def delete_attached_file
file = nil
save!
end
end

View file

@ -10,13 +10,41 @@ class MessageGateway < ActionMailer::Base
todo_builder = TodoFromRichMessage.new(user, context.id, todo_params[:description], todo_params[:notes])
todo = todo_builder.construct
todo.save!
Rails.logger.info "Saved email as todo for user #{user.login} in context #{context.name}"
if todo.save!
Rails.logger.info "Saved email as todo for user #{user.login} in context #{context.name}"
if attach_email_to_todo(todo, email)
Rails.logger.info "Saved email as attachment to todo for user #{user.login} in context #{context.name}"
end
end
todo
end
private
def attach_email_to_todo(todo, email)
attachment = todo.attachments.build
# create temp file
tmp = Tempfile.new(['attachment', '.eml'], {universal_newline: true})
tmp.write email.raw_source.gsub(/\r/, "")
# add temp file to attachment. paperclip will copy the file to the right location
Rails.logger.info "Saved received email to #{tmp.path}"
attachment.file = tmp
tmp.close
saved = attachment.save!
# enable write permissions on group, since MessageGateway could be run under different
# user than Tracks (i.e. apache versus mail)
dir = File.open(File.dirname(attachment.file.path))
dir.chmod(0770)
# delete temp file
tmp.unlink
end
def get_todo_params(email)
params = {}
@ -111,5 +139,4 @@ class MessageGateway < ActionMailer::Base
end
end
end
end

View file

@ -24,6 +24,7 @@ class Todo < ActiveRecord::Base
:source => :predecessor
has_many :pending_successors, -> {where('todos.state = ?', 'pending')}, :through => :predecessor_dependencies,
:source => :successor
has_many :attachments, dependent: :destroy
# scopes for states of this todo
scope :active, -> { where state: 'active' }

View file

@ -96,6 +96,7 @@ class User < ActiveRecord::Base
has_many :notes, -> { order "created_at DESC" }, dependent: :delete_all
has_one :preference, dependent: :destroy
has_many :attachments, through: :todos
validates_presence_of :login
validates_presence_of :password, if: :password_required?

View file

@ -41,6 +41,7 @@ cache [todo, current_user.date.strftime("%Y%m%d"), @source_view, current_user.pr
<%= project_and_context_links( todo, parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>
<%= collapsed_notes_image(todo) if todo.notes.present? %>
<%= collapsed_successors_image(todo) if todo.has_pending_successors %>
<%= attachment_image(todo) if todo.attachments.present? %>
</div>
</div>
<div id="<%= dom_id(todo, 'edit') %>" class="edit-form" style="display:none">

View file

@ -0,0 +1,10 @@
#config/initilizers/paperclip.rb
require 'paperclip/media_type_spoof_detector'
module Paperclip
class MediaTypeSpoofDetector
def spoofed?
false
end
end
end

View file

@ -101,11 +101,12 @@ Rails.application.routes.draw do
end
end
# match /todos/tag and put everything in :name, including extensions like .m and .txt.
# match /todos/tag and put everything in :name, including extensions like .m and .txt.
# This means the controller action needs to parse the extension and set format/content type
# Needed for /todos/tag/first.last.m to work
get 'todos/tag/:name' => 'todos#tag', :as => :tag, :format => false, :name => /.*/
get 'attachments/:id/:filename' => "todos#attachment"
get 'tags.autocomplete' => "todos#tags", :format => 'autocomplete'
get 'todos/done/tag/:name' => "todos#done_tag", :as => :done_tag
get 'todos/all_done/tag/:name' => "todos#all_done_tag", :as => :all_done_tag

View file

@ -0,0 +1,9 @@
class CreateAttachments < ActiveRecord::Migration
def change
create_table :attachments do |t|
t.references :todo, index: true
t.attachment :file
t.timestamps
end
end
end

View file

@ -11,11 +11,23 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150209233951) do
ActiveRecord::Schema.define(version: 20150805144100) do
create_table "attachments", force: true do |t|
t.integer "todo_id"
t.string "file_file_name"
t.string "file_content_type"
t.integer "file_file_size"
t.datetime "file_updated_at"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "attachments", ["todo_id"], name: "index_attachments_on_todo_id", using: :btree
create_table "contexts", force: true do |t|
t.string "name", null: false
t.integer "position"
t.integer "position", default: 0
t.integer "user_id", default: 1
t.datetime "created_at"
t.datetime "updated_at"
@ -85,11 +97,11 @@ ActiveRecord::Schema.define(version: 20150209233951) do
add_index "preferences", ["user_id"], name: "index_preferences_on_user_id", using: :btree
create_table "projects", force: true do |t|
t.string "name", null: false
t.integer "position"
t.integer "user_id", default: 1
t.text "description"
t.string "state", limit: 20, null: false
t.string "name", null: false
t.integer "position", default: 0
t.integer "user_id", default: 1
t.text "description", limit: 16777215
t.string "state", limit: 20, null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "default_context_id"
@ -104,17 +116,17 @@ ActiveRecord::Schema.define(version: 20150209233951) do
add_index "projects", ["user_id"], name: "index_projects_on_user_id", using: :btree
create_table "recurring_todos", force: true do |t|
t.integer "user_id", default: 1
t.integer "context_id", null: false
t.integer "user_id", default: 1
t.integer "context_id", null: false
t.integer "project_id"
t.string "description", null: false
t.text "notes"
t.string "state", limit: 20, null: false
t.string "description", null: false
t.text "notes", limit: 16777215
t.string "state", limit: 20, null: false
t.datetime "start_from"
t.string "ends_on"
t.datetime "end_date"
t.integer "number_of_occurrences"
t.integer "occurrences_count", default: 0
t.integer "occurrences_count", default: 0
t.string "target"
t.integer "show_from_delta"
t.string "recurring_period"
@ -123,7 +135,7 @@ ActiveRecord::Schema.define(version: 20150209233951) do
t.integer "every_other2"
t.integer "every_other3"
t.string "every_day"
t.boolean "only_work_days", default: false
t.boolean "only_work_days", default: false
t.integer "every_count"
t.integer "weekday"
t.datetime "completed_at"
@ -141,7 +153,7 @@ ActiveRecord::Schema.define(version: 20150209233951) do
t.datetime "updated_at"
end
add_index "sessions", ["session_id"], name: "index_sessions_on_session_id", using: :btree
add_index "sessions", ["session_id"], name: "sessions_session_id_index", using: :btree
create_table "taggings", force: true do |t|
t.integer "taggable_id"
@ -162,19 +174,19 @@ ActiveRecord::Schema.define(version: 20150209233951) do
add_index "tags", ["name"], name: "index_tags_on_name", using: :btree
create_table "todos", force: true do |t|
t.integer "context_id", null: false
t.integer "context_id", null: false
t.integer "project_id"
t.string "description", null: false
t.text "notes"
t.string "description", null: false
t.text "notes", limit: 16777215
t.datetime "created_at"
t.datetime "due"
t.datetime "completed_at"
t.integer "user_id", default: 1
t.integer "user_id", default: 1
t.datetime "show_from"
t.string "state", limit: 20, null: false
t.string "state", limit: 20, null: false
t.integer "recurring_todo_id"
t.datetime "updated_at"
t.text "rendered_notes"
t.text "rendered_notes", limit: 16777215
end
add_index "todos", ["context_id"], name: "index_todos_on_context_id", using: :btree
@ -212,7 +224,7 @@ ActiveRecord::Schema.define(version: 20150209233951) do
create_table "users", force: true do |t|
t.string "login", limit: 80, null: false
t.string "crypted_password", limit: 60, null: false
t.string "crypted_password", limit: 60
t.string "token"
t.boolean "is_admin", default: false, null: false
t.string "first_name"

View file

@ -13,18 +13,18 @@ class MessageGatewayTest < ActiveSupport::TestCase
def test_sms_with_no_subject
todo_count = Todo.count
load_message('sample_sms.txt')
# assert some stuff about it being created
assert_equal(todo_count+1, Todo.count)
message_todo = Todo.where(:description => "message_content").first
assert_not_nil(message_todo)
assert_equal(@inbox, message_todo.context)
assert_equal(@user, message_todo.user)
end
def test_mms_with_subject
todo_count = Todo.count
@ -40,7 +40,7 @@ class MessageGatewayTest < ActiveSupport::TestCase
assert_equal(@user, message_todo.user)
assert_equal("This is the message body", message_todo.notes)
end
def test_email_with_winmail_dat
todo_count = Todo.count
@ -58,7 +58,7 @@ class MessageGatewayTest < ActiveSupport::TestCase
# assert some stuff about it being created
assert_equal(todo_count+1, Todo.count)
end
def test_no_user
todo_count = Todo.count
badmessage = File.read(File.join(Rails.root, 'test', 'fixtures', 'sample_sms.txt'))
@ -66,21 +66,38 @@ class MessageGatewayTest < ActiveSupport::TestCase
MessageGateway.receive(badmessage)
assert_equal(todo_count, Todo.count)
end
def test_direct_to_context
message = File.read(File.join(Rails.root, 'test', 'fixtures', 'sample_sms.txt'))
valid_context_msg = message.gsub('message_content', 'this is a task @ anothercontext')
invalid_context_msg = message.gsub('message_content', 'this is also a task @ notacontext')
MessageGateway.receive(valid_context_msg)
valid_context_todo = Todo.where(:description => "this is a task").first
assert_not_nil(valid_context_todo)
assert_equal(contexts(:anothercontext), valid_context_todo.context)
MessageGateway.receive(invalid_context_msg)
invalid_context_todo = Todo.where(:description => 'this is also a task').first
assert_not_nil(invalid_context_todo)
assert_equal(@inbox, invalid_context_todo.context)
end
def test_receiving_email_adds_attachment
attachment_count = Attachment.count
load_message('sample_mms.txt')
message_todo = Todo.where(:description => "This is the subject").first
assert_not_nil(message_todo)
assert_equal attachment_count+1, Attachment.count
assert_equal 1,message_todo.attachments.count
orig = File.read(File.join(Rails.root, 'test', 'fixtures', 'sample_mms.txt'))
attachment = File.read(message_todo.attachments.first.file.path)
assert_equal orig, attachment
end
end

7
test/fixtures/attachments.yml vendored Normal file
View file

@ -0,0 +1,7 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
todo_id:
two:
todo_id:

View file

@ -0,0 +1,7 @@
require 'test_helper'
class AttachmentTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -72,26 +72,26 @@ class TodoTest < ActiveSupport::TestCase
def test_validate_show_from_must_be_a_date_in_the_future
t = @not_completed2
t.show_from = 1.week.ago
assert !t.save, "todo should not be saved without validation errors"
assert_equal 1, t.errors.count
assert_equal "must be a date in the future", t.errors[:show_from][0]
end
def test_validate_circular_dependencies
@completed.activate!
@not_completed3=@completed
# 2 -> 1
@not_completed1.add_predecessor(@not_completed2)
assert @not_completed1.save!
assert_equal 1, @not_completed2.successors.count
# 3 -> 2 -> 1
@not_completed2.add_predecessor(@not_completed3)
assert @not_completed2.save!
assert_equal 1, @not_completed3.successors.count
# 1 -> 3 -> 2 -> 1 == circle
assert_raises ActiveRecord::RecordInvalid do
@not_completed3.add_predecessor(@not_completed1)
@ -131,7 +131,7 @@ class TodoTest < ActiveSupport::TestCase
t.toggle_completion!
assert_equal :active, t.aasm.current_state
end
def test_toggle_completion_with_show_from_in_future
t = @not_completed1
t.show_from= 1.week.from_now
@ -140,12 +140,12 @@ class TodoTest < ActiveSupport::TestCase
t.toggle_completion!
assert_equal :completed, t.aasm.current_state
end
def test_toggle_completion_with_show_from_in_past
t = @not_completed1
t.update_attribute(:show_from, 1.week.ago)
assert_equal :active, t.aasm.current_state
assert t.toggle_completion!, "shoud be able to mark active todo complete even if show_from is set in the past"
assert_equal :completed, t.aasm.current_state
end
@ -219,7 +219,7 @@ class TodoTest < ActiveSupport::TestCase
# And I update the state of the todo from its project
new_todo.update_state_from_project
# Then the todo should be hidden
assert new_todo.hidden?
assert new_todo.hidden?
end
def test_initial_state_defaults_to_active
@ -280,7 +280,7 @@ class TodoTest < ActiveSupport::TestCase
assert todo.pending?, "todo with predecessor should be blocked"
# cannot activate if part of hidden project
assert_raise(AASM::InvalidTransition) { todo.activate! }
assert_raise(AASM::InvalidTransition) { todo.activate! }
todo.remove_predecessor(todo2)
assert todo.reload.hidden?, "todo should be put back in hidden state"
@ -337,7 +337,7 @@ class TodoTest < ActiveSupport::TestCase
@not_completed1.add_predecessor(@not_completed2)
@not_completed1.save_predecessors
# blocking is not done automagically
@not_completed1.block!
@not_completed1.block!
assert @not_completed1.uncompleted_predecessors?
assert @not_completed1.pending?, "a todo with predecessors should be pending"
@ -358,7 +358,7 @@ class TodoTest < ActiveSupport::TestCase
@not_completed1.add_predecessor_list("#{@not_completed2.id}, #{@not_completed3.id}")
@not_completed1.save_predecessors
# blocking is not done automagically
@not_completed1.block!
@not_completed1.block!
# Then @completed1 should have predecessors and should be blocked
assert @not_completed1.uncompleted_predecessors?
@ -526,20 +526,43 @@ class TodoTest < ActiveSupport::TestCase
assert !older_created_todos.include?(todo_now)
assert !recent_created_todos.include?(todo_old)
end
def test_notes_are_rendered_on_save
user = @completed.user
todo = user.todos.create(:description => "test", :context => @completed.context)
assert_nil todo.notes
assert_nil todo.rendered_notes
todo.notes = "*test*"
todo.save!
todo.reload
assert_equal "*test*", todo.notes
assert_equal "<p><strong>test</strong></p>", todo.rendered_notes
end
def test_attachments_are_removed_after_delete
# Given a user and a todo withou any attachments
todo = @not_completed1
assert_equal 0, todo.attachments.count, "we start without attachments"
assert_equal 0, todo.user.attachments.count, "the user has no attachments"
# When I add a file as attachment to a todo of this user
attachment = todo.attachments.build
attachment.file = File.open(File.join(Rails.root, 'test', 'fixtures', 'email_with_multipart.txt'))
attachment.save!
new_path = attachment.file.path
# then the attachment should be there
assert File.exists?(new_path), "attachment should be on file system"
assert_equal 1, todo.attachments.reload.count, "should have one attachment"
# When I destroy the todo
todo.destroy!
# Then the attachement and file should nogt be there anymore
assert_equal 0, todo.user.attachments.reload.count
assert !File.exists?(new_path), "attachment should not be on file system"
end
end