From 5499ac2a031a7e4d3bb2c5220dfe50d99d2cc7a8 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 5 Aug 2015 12:58:52 +0200 Subject: [PATCH 1/4] Add paperclip gem --- Gemfile | 1 + Gemfile.lock | 12 ++++++++++++ config/initializers/paperclip.rb | 10 ++++++++++ 3 files changed, 23 insertions(+) create mode 100644 config/initializers/paperclip.rb 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/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 From 2bd68fecb760c67fe792cae5bdb391b853b6f13e Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 5 Aug 2015 13:01:02 +0200 Subject: [PATCH 2/4] Create attachment model and hook it up to todo An attachment has write permissions on group so that managing attachments work from different users in same group, i.e. user apache and user mail. --- .gitignore | 1 + app/models/attachment.rb | 20 +++++++ app/models/todo.rb | 1 + app/models/user.rb | 1 + config/routes.rb | 3 +- .../20150805144100_create_attachments.rb | 9 +++ db/schema.rb | 56 +++++++++++-------- test/fixtures/attachments.yml | 7 +++ test/models/attachment_test.rb | 7 +++ test/models/todo_test.rb | 55 ++++++++++++------ 10 files changed, 121 insertions(+), 39 deletions(-) create mode 100644 app/models/attachment.rb create mode 100644 db/migrate/20150805144100_create_attachments.rb create mode 100644 test/fixtures/attachments.yml create mode 100644 test/models/attachment_test.rb 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/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/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/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/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 From f2c6c2d3af572276ef66efa4d713802b04be0a3d Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 5 Aug 2015 13:02:41 +0200 Subject: [PATCH 3/4] show attachment in view and make it downloadable --- app/assets/stylesheets/tracks.css.scss | 5 +++++ app/controllers/todos_controller.rb | 14 ++++++++++++++ app/helpers/todos_helper.rb | 8 ++++++++ app/views/todos/_todo.html.erb | 1 + 4 files changed, 28 insertions(+) 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/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? %>