From 2f043911c6a700892c62ff4985451c947f292ea1 Mon Sep 17 00:00:00 2001 From: Greg Sutcliffe Date: Sun, 13 Oct 2013 22:11:55 +0100 Subject: [PATCH] Extend RichMessage format to include other data Uses new Regex to detect: @ context ~ project > tickler-date < due-date # tag (repeatable) * (starred) --- app/models/message_gateway.rb | 8 +-- app/models/project.rb | 1 + app/services/rich_message_extractor.rb | 60 +++++++++++++++++--- app/services/todo_from_rich_message.rb | 20 +++++-- app/views/integrations/index.en.html.erb | 32 +++++++++++ test/models/rich_message_extractor_test.rb | 65 ++++++++++++++++++++-- test/models/todo_from_rich_message_test.rb | 19 +++++++ 7 files changed, 180 insertions(+), 25 deletions(-) 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..9350e8b5 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 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.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 @@
  • Automatically Email Yourself Upcoming Actions
  • Integrate Tracks with an email server to be able to send an action through email to Tracks
  • Send emails to Tracks with Mailgun +
  • Rich Todo Message email format
  • Add Tracks as a Google Gmail gadget

  • Do you have one of your own to add? @@ -122,6 +123,7 @@

    Send emails to Tracks with Mailgun

    +

    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

    + +

    Rich Todo Message Format

    +

    For both of the above methods, the follow format can be used:

    +
    my awesome todo @context ~project <131012 >131009 #tag1 #tag2 *
    +

    The fields are:

    + + + + + + + + + + + + + + + + + + + + + + +
    SymbolMeaning
    @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

    +

    Add Tracks as a Google Gmail gadget

    diff --git a/test/models/rich_message_extractor_test.rb b/test/models/rich_message_extractor_test.rb index 5c264685..3376c289 100644 --- a/test/models/rich_message_extractor_test.rb +++ b/test/models/rich_message_extractor_test.rb @@ -1,3 +1,4 @@ +require 'date' require 'test/unit' require 'active_support/core_ext/object/blank' require_relative '../../app/services/rich_message_extractor.rb' @@ -5,11 +6,15 @@ require_relative '../../app/services/rich_message_extractor.rb' class RichMessageExtractorTest < Test::Unit::TestCase def test_message_with_all_options - message = "ohai@some-context>in-this-project" + message = "ohai@some-context~this-project>131012<131014#tag1#tag2*" extractor = RichMessageExtractor.new(message) assert_equal "ohai", extractor.description assert_equal "some-context", extractor.context - assert_equal "in-this-project", extractor.project + assert_equal "this-project", extractor.project + assert_equal "2013-10-12", extractor.show_from.to_s + assert_equal "2013-10-14", extractor.due.to_s + assert_equal ["tag1","tag2"], extractor.tags + assert extractor.starred? end def test_message_without_project @@ -20,12 +25,12 @@ class RichMessageExtractorTest < Test::Unit::TestCase assert_equal nil, extractor.project end - def test_message_without_project - message = " ohai @ some-context" + def test_message_without_context + message = " ohai ~ some-project" extractor = RichMessageExtractor.new(message) assert_equal "ohai", extractor.description - assert_equal "some-context", extractor.context - assert_equal nil, extractor.project + assert_equal "", extractor.context + assert_equal "some-project", extractor.project end def test_message_without_project_or_context @@ -52,4 +57,52 @@ class RichMessageExtractorTest < Test::Unit::TestCase assert_equal nil, extractor.project end + def test_message_with_tags + message = "some tags#tag 1#tag2" + extractor = RichMessageExtractor.new(message) + assert_equal ["tag 1","tag2"], extractor.tags + end + + def test_message_with_no_tags + message = "no tags" + extractor = RichMessageExtractor.new(message) + assert_equal nil, extractor.tags + end + + def test_message_with_due_date + message = "datetest<141013" + extractor = RichMessageExtractor.new(message) + assert_equal "2014-10-13", extractor.due.to_s + end + + def test_message_with_no_due_date + message = "no date" + extractor = RichMessageExtractor.new(message) + assert_equal nil, extractor.due + end + + def test_message_with_show_from + message = "datetest>161013" + extractor = RichMessageExtractor.new(message) + assert_equal "2016-10-13", extractor.show_from.to_s + end + + def test_message_with_no_show_from + message = "no tickler" + extractor = RichMessageExtractor.new(message) + assert_equal nil, extractor.show_from + end + + def test_message_with_star + message = "star test*" + extractor = RichMessageExtractor.new(message) + assert extractor.starred? + end + + def test_message_with_no_star + message = "no star test" + extractor = RichMessageExtractor.new(message) + refute extractor.starred? + end + end diff --git a/test/models/todo_from_rich_message_test.rb b/test/models/todo_from_rich_message_test.rb index f9206624..2b57da38 100644 --- a/test/models/todo_from_rich_message_test.rb +++ b/test/models/todo_from_rich_message_test.rb @@ -18,4 +18,23 @@ class TodoFromRichMessageTest < ActiveSupport::TestCase assert_equal default_context_id, new_todo.context_id end + def test_from_rich_message_adds_all_fields + user = @completed.user + context = Context.create(:name => 'context') + project = Project.create(:name => 'project') + message = "description@context~project>131014<131017#tag1#tag2*" + builder = TodoFromRichMessage.new(user, context.id, message, "notes") + new_todo = builder.construct + + assert_not_nil new_todo + assert_equal "description", new_todo.description + assert_equal "notes", new_todo.notes + assert_equal context.id, new_todo.context_id + assert_equal project.id, new_todo.project_id + assert_equal "2013-10-14 00:00:00 +0100", new_todo.show_from.to_s + assert_equal "2013-10-17 00:00:00 +0100", new_todo.due.to_s + assert_equal "starred, tag1, tag2", new_todo.tags.to_s + assert new_todo.starred? + end + end