diff --git a/.gitignore b/.gitignore
index e200dc5c..c7069277 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/db/*.db
/db/*.sqlite3
/db/*.sqlite3-journal
+/db/assets/*
/log/*.log
/public/assets/
/tmp
diff --git a/Gemfile b/Gemfile
index 5b07e9c7..98913b0a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 909d1115..aafd4049 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/assets/stylesheets/tracks.css.scss b/app/assets/stylesheets/tracks.css.scss
index 3bb46e3c..6cbd2653 100644
--- a/app/assets/stylesheets/tracks.css.scss
+++ b/app/assets/stylesheets/tracks.css.scss
@@ -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; }
diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb
index 515450f6..0b6fbd1c 100644
--- a/app/controllers/todos_controller.rb
+++ b/app/controllers/todos_controller.rb
@@ -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
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 75a651ad..7bec8b48 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -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' ),
diff --git a/app/models/attachment.rb b/app/models/attachment.rb
new file mode 100644
index 00000000..564a0ed6
--- /dev/null
+++ b/app/models/attachment.rb
@@ -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
diff --git a/app/models/message_gateway.rb b/app/models/message_gateway.rb
index 11ea3167..de60e8df 100644
--- a/app/models/message_gateway.rb
+++ b/app/models/message_gateway.rb
@@ -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
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3f82e217..7842a7a6 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -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' }
diff --git a/app/models/user.rb b/app/models/user.rb
index 49bc4c2e..d693c0f1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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?
diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb
index 07e8da70..13335d37 100644
--- a/app/views/todos/_todo.html.erb
+++ b/app/views/todos/_todo.html.erb
@@ -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? %>
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
new file mode 100644
index 00000000..11e77531
--- /dev/null
+++ b/config/initializers/paperclip.rb
@@ -0,0 +1,10 @@
+#config/initilizers/paperclip.rb
+require 'paperclip/media_type_spoof_detector'
+
+module Paperclip
+ class MediaTypeSpoofDetector
+ def spoofed?
+ false
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 11323624..84de0f15 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20150805144100_create_attachments.rb b/db/migrate/20150805144100_create_attachments.rb
new file mode 100644
index 00000000..277cfb19
--- /dev/null
+++ b/db/migrate/20150805144100_create_attachments.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 37555542..6f647e2a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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"
diff --git a/test/controllers/message_gateway_test.rb b/test/controllers/message_gateway_test.rb
index d64f6adc..6bb3eab9 100644
--- a/test/controllers/message_gateway_test.rb
+++ b/test/controllers/message_gateway_test.rb
@@ -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
diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml
new file mode 100644
index 00000000..1a710b20
--- /dev/null
+++ b/test/fixtures/attachments.yml
@@ -0,0 +1,7 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ todo_id:
+
+two:
+ todo_id:
diff --git a/test/models/attachment_test.rb b/test/models/attachment_test.rb
new file mode 100644
index 00000000..ff491dc1
--- /dev/null
+++ b/test/models/attachment_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class AttachmentTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/models/todo_test.rb b/test/models/todo_test.rb
index dcdf1a2d..f137411e 100644
--- a/test/models/todo_test.rb
+++ b/test/models/todo_test.rb
@@ -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 "
test
", 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