diff --git a/.travis.yml b/.travis.yml index a8879b52..5eeee3cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ language: ruby + rvm: - 1.9.3 - 2.0.0 + - 2.1 + +bundler_args: --without development before_install: +before_install: + - "gem install bundler -v=1.5.1" - "mysql -e 'create database tracks_test;'" - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" @@ -12,7 +18,6 @@ before_install: script: "CODECLIMATE_REPO_TOKEN=5c52fdd2bbcd0734d56ddb2c3cbaac782da345273e8689d25f54a065ccc3397c bundle exec rake ci RACK_ENV=test" -bundler_args: --without development notifications: email: false diff --git a/Gemfile.lock b/Gemfile.lock index 12b4e500..9106e1c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/cucumber/aruba - revision: adbfc240d69254d7b525876b4c5bff6b721b7d65 + revision: 7afbc5c0cbae9c9a946d70c4c2735ccb86e00f08 specs: aruba (0.5.3) childprocess (>= 0.3.6) @@ -9,35 +9,35 @@ GIT GIT remote: https://github.com/rails/actionpack-xml_parser - revision: 246653ab3670f329176c1e77e6cd1a632466f06e + revision: e1516064761ea26502cd79b283f6af0fa2b1edf5 specs: - actionpack-xml_parser (1.0.0) - actionpack (>= 4.0.0.rc1, < 4.1) + actionpack-xml_parser (1.0.1) + actionpack (>= 4.0.0, < 5) GEM remote: https://rubygems.org/ specs: RedCloth (4.2.9) - aasm (3.0.22) - actionmailer (4.0.0) - actionpack (= 4.0.0) - mail (~> 2.5.3) - actionpack (4.0.0) - activesupport (= 4.0.0) + aasm (3.0.25) + actionmailer (4.0.2) + actionpack (= 4.0.2) + mail (~> 2.5.4) + actionpack (4.0.2) + activesupport (= 4.0.2) builder (~> 3.1.0) erubis (~> 2.7.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - activemodel (4.0.0) - activesupport (= 4.0.0) + activemodel (4.0.2) + activesupport (= 4.0.2) builder (~> 3.1.0) - activerecord (4.0.0) - activemodel (= 4.0.0) + activerecord (4.0.2) + activemodel (= 4.0.2) activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.0) + activesupport (= 4.0.2) arel (~> 4.0.0) activerecord-deprecated_finders (1.0.3) - activesupport (4.0.0) + activesupport (4.0.2) i18n (~> 0.6, >= 0.6.4) minitest (~> 4.2) multi_json (~> 1.3) @@ -45,16 +45,17 @@ GEM tzinfo (~> 0.3.37) acts_as_list (0.3.0) activerecord (>= 3.0) - arel (4.0.0) + arel (4.0.1) atomic (1.1.14) bcrypt-ruby (3.0.1) builder (3.1.4) - bullet (4.6.0) - uniform_notifier + bullet (4.7.1) + activesupport + uniform_notifier (>= 1.4.0) cache_digests (0.3.1) actionpack (>= 3.2) thread_safe - capybara (2.1.0) + capybara (2.2.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -62,20 +63,20 @@ GEM xpath (~> 2.0) childprocess (0.3.9) ffi (~> 1.0, >= 1.0.11) - codeclimate-test-reporter (0.1.1) + codeclimate-test-reporter (0.2.0) simplecov (>= 0.7.1, < 1.0.0) - coffee-rails (4.0.0) + coffee-rails (4.0.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0.beta, < 5.0) + railties (>= 4.0.0, < 5.0) coffee-script (2.2.0) coffee-script-source execjs coffee-script-source (1.6.3) commonjs (0.2.7) - cucumber (1.3.8) + cucumber (1.3.10) builder (>= 2.1.2) diff-lcs (>= 1.1.3) - gherkin (~> 2.12.1) + gherkin (~> 2.12) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.0.2) cucumber-rails (1.4.0) @@ -83,25 +84,26 @@ GEM cucumber (>= 1.2.0) nokogiri (>= 1.5.0) rails (>= 3.0.0) - database_cleaner (1.1.1) - diff-lcs (1.2.4) + database_cleaner (1.2.0) + diff-lcs (1.2.5) + docile (1.1.1) erubis (2.7.0) - execjs (2.0.1) - factory_girl (4.2.0) + execjs (2.0.2) + factory_girl (4.3.0) activesupport (>= 3.0.0) - factory_girl_rails (4.2.1) - factory_girl (~> 4.2.0) + factory_girl_rails (4.3.0) + factory_girl (~> 4.3.0) railties (>= 3.0.0) - ffi (1.9.0) - gherkin (2.12.1) + ffi (1.9.3) + gherkin (2.12.2) multi_json (~> 1.3) hike (1.2.3) htmlentities (4.3.1) - i18n (0.6.5) + i18n (0.6.9) jquery-rails (3.0.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.0) + json (1.8.1) less (2.4.0) commonjs (~> 0.2.7) less-rails (2.4.2) @@ -112,66 +114,69 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) metaclass (0.0.1) - mime-types (1.25) - mini_portile (0.5.1) + mime-types (1.25.1) + mini_portile (0.5.2) minitest (4.7.5) mocha (0.14.0) metaclass (~> 0.0.1) - mousetrap-rails (0.0.12) - multi_json (1.8.0) - multi_test (0.0.2) - mysql2 (0.3.13) - nokogiri (1.6.0) + mousetrap-rails (1.4.6) + multi_json (1.8.2) + multi_test (0.0.3) + mysql2 (0.3.14) + nokogiri (1.6.1) mini_portile (~> 0.5.0) polyglot (0.3.3) + protected_attributes (1.0.5) + activemodel (>= 4.0.1, < 5.0) rack (1.5.2) - rack-mini-profiler (0.1.31) + rack-mini-profiler (0.9.0) rack (>= 1.1.3) rack-test (0.6.2) rack (>= 1.0) - rails (4.0.0) - actionmailer (= 4.0.0) - actionpack (= 4.0.0) - activerecord (= 4.0.0) - activesupport (= 4.0.0) + rails (4.0.2) + actionmailer (= 4.0.2) + actionpack (= 4.0.2) + activerecord (= 4.0.2) + activesupport (= 4.0.2) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.0) + railties (= 4.0.2) sprockets-rails (~> 2.0.0) - rails_autolink (1.1.4) + rails_autolink (1.1.5) rails (> 3.1) - railties (4.0.0) - actionpack (= 4.0.0) - activesupport (= 4.0.0) + railties (4.0.2) + actionpack (= 4.0.2) + activesupport (= 4.0.2) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.0) + rake (10.1.1) ref (1.0.5) - rspec-expectations (2.14.3) + rspec-expectations (2.14.4) diff-lcs (>= 1.1.3, < 2.0) - rubyzip (0.9.9) + rubyzip (1.1.0) safe_yaml (0.9.7) sanitize (2.0.6) nokogiri (>= 1.4.4) - sass (3.2.10) - sass-rails (4.0.0) - railties (>= 4.0.0.beta, < 5.0) + sass (3.2.13) + sass-rails (4.0.1) + railties (>= 4.0.0, < 5.0) sass (>= 3.1.10) sprockets-rails (~> 2.0.0) - selenium-webdriver (2.35.1) + selenium-webdriver (2.39.0) childprocess (>= 0.2.5) multi_json (~> 1.0) - rubyzip (< 1.0.0) + rubyzip (~> 1.0) websocket (~> 1.0.4) - simplecov (0.7.1) - multi_json (~> 1.0) - simplecov-html (~> 0.7.1) - simplecov-html (0.7.1) - sprockets (2.10.0) + simplecov (0.8.2) + docile (~> 1.1.0) + multi_json + simplecov-html (~> 0.8.0) + simplecov-html (0.8.0) + sprockets (2.10.1) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.0) + sprockets-rails (2.0.1) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) @@ -187,31 +192,32 @@ GEM atomic tilt (1.4.1) timecop (0.6.3) - tolk (1.3.11) + tolk (1.4.00) + protected_attributes safe_yaml (~> 0.8) will_paginate treetop (1.4.15) polyglot polyglot (>= 0.3.1) - turbolinks (1.3.0) + turbolinks (2.1.0) coffee-rails twitter-bootstrap-rails (2.2.8) actionpack (>= 3.1) execjs rails (>= 3.1) railties (>= 3.1) - tzinfo (0.3.37) - uglifier (2.2.1) + tzinfo (0.3.38) + uglifier (2.4.0) execjs (>= 0.3.0) - multi_json (~> 1.0, >= 1.0.2) - uniform_notifier (1.3.0) + json (>= 1.8.0) + uniform_notifier (1.4.0) websocket (1.0.7) will_paginate (3.0.5) - will_paginate-bootstrap (0.2.5) + will_paginate-bootstrap (1.0.0) will_paginate (>= 3.0.3) xpath (2.0.0) nokogiri (~> 1.3) - yard (0.8.7.2) + yard (0.8.7.3) PLATFORMS ruby diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f98ac793..a5694db6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -95,7 +95,7 @@ class ApplicationController < ActionController::Base if todos_parent.nil? count = 0 elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) - count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" + count = @project_project_hidden_todo_counts[todos_parent.id] else count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 6350d16d..24eafe55 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -747,34 +747,29 @@ class TodosController < ApplicationController end end + def get_not_completed_for_predecessor(relation, todo_id=nil) + items = relation.todos.not_completed. + where('(LOWER(todos.description) LIKE ?)', "%#{params[:term].downcase}%") + items = items.where("AND NOT(todos.id=?)", todo_id) unless todo_id.nil? + + items. + includes(:context, :project). + reorder('description ASC'). + limit(10) + end + def auto_complete_for_predecessor unless params['id'].nil? get_todo_from_params # Begin matching todos in current project, excluding @todo itself - @items = @todo.project.todos.not_completed. - where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). - includes(:context, :project). - reorder('description ASC'). - limit(10) unless @todo.project.nil? + @items = get_not_completed_for_predecessor(@todo.project, @todo.id) unless @todo.project.nil? # Then look in the current context, excluding @todo itself - @items = @todo.context.todos.not_completed. - where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). - includes(:context, :project). - reorder('description ASC'). - limit(10) unless !@items.empty? || @todo.context.nil? + @items = get_not_completed_for_predecessor(@todo.context, @todo.id) unless !@items.empty? || @todo.context.nil? # Match todos in other projects, excluding @todo itself - @items = current_user.todos.not_completed. - where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). - includes(:context, :project). - reorder('description ASC'). - limit(10) unless !@items.empty? + @items = get_not_completed_for_predecessor(current_user, @todo.id) unless !@items.empty? else # New todo - TODO: Filter on current project in project view - @items = current_user.todos.not_completed. - where('(LOWER(todos.description) LIKE ?)', "%#{params[:term].downcase}%"). - includes(:context, :project). - reorder('description ASC'). - limit(10) + @items = get_not_complete_for_predecessor(current_user) end render :inline => format_dependencies_as_json_for_auto_complete(@items) end @@ -1020,27 +1015,23 @@ end end end + def find_completed(relation, id, include_hidden) + todos = relation.find(id).todos.completed + todos = todos.not_hidden if !include_hidden + return todos + end + def determine_completed_count + todos=nil + source_view do |from| - from.todo do - @completed_count = current_user.todos.not_hidden.completed.count - end - from.context do - todos = current_user.contexts.find(@todo.context_id).todos.completed - todos = todos.not_hidden if !@todo.context.hidden? - @completed_count = todos.count - end - from.project do - unless @todo.project_id == nil - todos = current_user.projects.find(@todo.project_id).todos.completed - todos = todos.not_hidden if !@todo.project.hidden? - @completed_count = todos.count - end - end - from.tag do - @completed_count = current_user.todos.with_tag(@tag.id).completed.count - end + from.todo { todos = current_user.todos.not_hidden.completed } + from.context { todos = find_completed(current_user.contexts, @todo.context_id, @todo.context.hidden?) } + from.project { todos = find_completed(current_user.projects, @todo.project_id, @todo.project.hidden?) unless @todo.project_id.nil? } + from.tag { todos = current_user.todos.with_tag(@tag.id).completed } end + + @completed_count = todos.nil? ? 0 : todos.count end def determine_deferred_tag_count(tag_name) @@ -1195,23 +1186,20 @@ end end end + def parse_date_for_update(date, error_msg) + begin + parse_date_per_user_prefs(date) + rescue + @todo.errors[:base] << error_msg + end + end + + def update_date_for_update(key) + params['todo'][key] = params["todo"].has_key?(key) ? parse_date_for_update(params["todo"][key], t("todos.error.invalid_#{key}_date")) : "" + end + def update_due_and_show_from_dates - if params["todo"].has_key?("due") - begin - params["todo"]["due"] = parse_date_per_user_prefs(params["todo"]["due"]) - rescue - @todo.errors[:base] << "Invalid due date" - end - else - params["todo"]["due"] = "" - end - if params['todo']['show_from'] - begin - params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from']) - rescue - @todo.errors[:base] << "Invalid show from date" - end - end + %w{ due show_from }.each {|date| update_date_for_update(date) } end def update_completed_state @@ -1283,18 +1271,18 @@ end completed_todos.completed_after(start_of_this_day).includes(includes[:include]) end + def get_done_in_period(before, after, includes = {:include => Todo::DEFAULT_INCLUDES}) + completed_todos.completed_before(before).completed_after(after).includes(includes[:include]) + end + # all completed todos [begin_of_week, start_of_today] def get_done_rest_of_week(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) - start_of_this_week = Time.zone.now.beginning_of_week - start_of_this_day = Time.zone.now.beginning_of_day - completed_todos.completed_before(start_of_this_day).completed_after(start_of_this_week).includes(includes[:include]) + get_done_in_period(Time.zone.now.beginning_of_day, Time.zone.now.beginning_of_week) end # all completed todos [begin_of_month, begin_of_week] def get_done_rest_of_month(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) - start_of_this_month = Time.zone.now.beginning_of_month - start_of_this_week = Time.zone.now.beginning_of_week - completed_todos.completed_before(start_of_this_week).completed_after(start_of_this_month).includes(includes[:include]) + get_done_in_period(Time.zone.now.beginning_of_week, Time.zone.now.beginning_of_month) end def get_not_done_todos diff --git a/app/models/message_gateway.rb b/app/models/message_gateway.rb index fb42dc92..b020c736 100644 --- a/app/models/message_gateway.rb +++ b/app/models/message_gateway.rb @@ -1,6 +1,4 @@ class MessageGateway < ActionMailer::Base - include ActionView::Helpers::SanitizeHelper - extend ActionView::Helpers::SanitizeHelper::ClassMethods def receive(email) user = get_receiving_user_from_email_address(email) @@ -85,11 +83,11 @@ class MessageGateway < ActionMailer::Base end def get_text_or_nil(text) - return text ? sanitize(text.strip) : nil + return text ? text.strip : nil end def get_decoded_text_or_nil(text) - return text ? sanitize(text.decoded.strip) : nil + return text ? text.decoded.strip : nil end def get_first_text_plain_part(email) @@ -99,7 +97,7 @@ class MessageGateway < ActionMailer::Base # remove all parts that are not text/plain parts.reject{|part| !part.content_type.start_with?("text/plain") } - return parts.count > 0 ? sanitize(parts[0].decoded.strip) : "" + return parts.count > 0 ? parts[0].decoded.strip : "" end def get_all_parts(parts) diff --git a/app/models/project.rb b/app/models/project.rb index 685c51c1..f60718ed 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ActiveRecord::Base scope :uncompleted, -> { where("NOT(state = ?)", 'completed') } scope :with_name_or_description, lambda { |body| where("name LIKE ? OR description LIKE ?", body, body) } + scope :with_namepart, lambda { |body| where("name LIKE ?", body + '%') } validates_presence_of :name validates_length_of :name, :maximum => 255 @@ -88,7 +89,7 @@ class Project < ActiveRecord::Base # as a result of acts_as_state_machine calling state=() to update the attribute def transition_to(candidate_state) case candidate_state.to_sym - when aasm_current_state + when aasm.current_state return when :hidden hide! diff --git a/app/models/todo.rb b/app/models/todo.rb index 66b9550a..2c37cf4b 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -66,7 +66,7 @@ class Todo < ActiveRecord::Base # state machine include AASM - aasm_initial_state Proc.new { |t| (t.show_from && t.user && (t.show_from > t.user.date)) ? :deferred : :active} + aasm_initial_state = Proc.new { |t| (t.show_from && t.user && (t.show_from > t.user.date)) ? :deferred : :active} aasm :column => :state do diff --git a/app/services/rich_message_extractor.rb b/app/services/rich_message_extractor.rb index a357b66d..e248ad1e 100644 --- a/app/services/rich_message_extractor.rb +++ b/app/services/rich_message_extractor.rb @@ -1,28 +1,72 @@ +require 'date' class RichMessageExtractor + include ActionView::Helpers::SanitizeHelper + extend ActionView::Helpers::SanitizeHelper::ClassMethods - RICH_MESSAGE_FIELDS_REGEX = /([^>@]*)@?([^>]*)>?(.*)/ + PROJECT_MARKER = '~' + CONTEXT_MARKER = '@' + TICKLER_MARKER = '>' + DUE_MARKER = '<' + TAG_MARKER = '#' + STAR_MARKER = '*' + + ALL_MARKERS = [ + PROJECT_MARKER, + CONTEXT_MARKER, + TICKLER_MARKER, + DUE_MARKER, + TAG_MARKER, + STAR_MARKER + ] def initialize(message) @message = message end def description - fields[1].strip + desc = select_for('') + desc.blank? ? '' : sanitize(desc[1].strip) end def context - fields[2].strip + context = select_for(CONTEXT_MARKER) + context.blank? ? '' : sanitize(context[1].strip) end def project - stripped = fields[3].strip - stripped.blank? ? nil : stripped + project = select_for PROJECT_MARKER + project.blank? ? nil : sanitize(project[1].strip) + end + + def tags + string = @message.dup + tags = [] + # Regex only matches one tag, so recurse until we have them all + while string.match /#(.*?)(?=[#{ALL_MARKERS.join}]|\Z)/ + tags << sanitize($1) + string.gsub!(/##{$1}/,'') + end + tags.empty? ? nil : tags + end + + def due + due = select_for DUE_MARKER + due.blank? ? nil : Date.parse(due[1].strip) + end + + def show_from + show_from = select_for TICKLER_MARKER + show_from.blank? ? nil : Date.parse(show_from[1].strip) + end + + def starred? + @message.include? '*' end private - def fields - @message.match(RICH_MESSAGE_FIELDS_REGEX) - end + def select_for symbol + @message.match /#{symbol}(.*?)(?=[#{ALL_MARKERS.join}]|\Z)/ + end end diff --git a/app/services/todo_from_rich_message.rb b/app/services/todo_from_rich_message.rb index f919b0bb..b1a28f22 100644 --- a/app/services/todo_from_rich_message.rb +++ b/app/services/todo_from_rich_message.rb @@ -14,6 +14,10 @@ class TodoFromRichMessage description = extractor.description context = extractor.context project = extractor.project + show_from = extractor.show_from + due = extractor.due + tags = extractor.tags + star = extractor.starred? context_id = default_context_id if context.present? @@ -33,17 +37,21 @@ class TodoFromRichMessage found_project.name = project[4..259].strip found_project.save! else - found_project = user.projects.active.find_by_namepart(project) - found_project = user.projects.find_by_namepart(project) if found_project.nil? + found_project = user.projects.active.with_namepart(project).first + found_project = user.projects.with_namepart(project).first if found_project.nil? end project_id = found_project.id unless found_project.nil? end - todo = user.todos.build + todo = user.todos.build todo.description = description - todo.raw_notes = notes - todo.context_id = context_id - todo.project_id = project_id unless project_id.nil? + todo.raw_notes = notes + todo.context_id = context_id + todo.project_id = project_id unless project_id.nil? + todo.show_from = show_from if show_from.is_a? Date + todo.due = due if due.is_a? Date + todo.tag_with tags unless tags.nil? || tags.empty? + todo.starred = star todo end end diff --git a/app/views/integrations/index.de.html.erb b/app/views/integrations/index.de.html.erb index 04850541..efaa2136 100644 --- a/app/views/integrations/index.de.html.erb +++ b/app/views/integrations/index.de.html.erb @@ -131,5 +131,5 @@
<%= integrations_url + "/google_gadget" %>
<%= integrations_url + "/google_gadget.xml" %>\ No newline at end of file diff --git a/app/views/integrations/index.en.html.erb b/app/views/integrations/index.en.html.erb index 6fd212b3..0dc900a2 100644 --- a/app/views/integrations/index.en.html.erb +++ b/app/views/integrations/index.en.html.erb @@ -14,6 +14,7 @@
Do you have one of your own to add? @@ -122,6 +123,7 @@
If you want to email tasks to Tracks, but cannot run a mailserver on the same host, you could use the Mailgun support built in to Tracks.
@@ -155,6 +157,36 @@ mailmap:All the comments about the email format from the section above apply to the Mailgun handling, as the data is processed the same way
+ +For both of the above methods, the follow format can be used:
+my awesome todo @context ~project <131012 >131009 #tag1 #tag2 *+
The fields are:
+| Symbol | Meaning | +
|---|---|
| @ | The context to place the Todo in | +
| ~ | The project to place the Todo in | +
| < | The due date for the Todo (may be 2 digits for day, 4 digits for month-day, or 6 digits for yeah-month-day) | +
| > | The due date for the Todo (may be 2 digits for day, 4 digits for month-day, or 6 digits for yeah-month-day) | +
| # | A tag to apply to the Todo - may be used multiple times | +
| * | Flag to star the Todo | +
All symbols are optional, and text up to the first symbol (or end of string) is used as the description of the todo
+diff --git a/app/views/projects/show.m.erb b/app/views/projects/show.m.erb index 8250b462..f289ca28 100644 --- a/app/views/projects/show.m.erb +++ b/app/views/projects/show.m.erb @@ -28,4 +28,4 @@ <% else -%><%= render :partial => "notes/notes_summary", :collection => @project.notes %> <% end -%>