Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Search feature more or less works

  • Loading branch information...
commit 1e69f3b664ca430e4b2c1502c645d67ae43d9387 1 parent 42eadcf
@ddemaree authored
View
13 app/controllers/application_controller.rb
@@ -59,6 +59,19 @@ def current_timer
@current_timer ||= current_account.timers.current
end
helper_method :current_timer
+
+ def event_params
+ params[:tagged] ||= (params[:tag] || params[:tags])
+ @event_params ||= Event::Params.new(params)
+ end
+ helper_method :event_params
+
+ def current_project
+ @current_project = current_account.trackables.find_by_id(event_params[:project]) ||
+ current_account.trackables.find_by_nickname(event_params[:project]) ||
+ current_account.trackables.find_by_name(event_params[:project])
+ end
+ helper_method :current_project
protected
View
23 app/controllers/events_controller.rb
@@ -4,21 +4,16 @@ class EventsController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, :with => :respond_on_invalid_event
def index
- session[:events_view] = {
- :controller => 'events',
- :page => params[:page],
- :per_page => params[:per_page],
- :tags => params[:tags],
- :trackable_id => params[:trackable_id],
- :starred => !!params[:starred]
- }
-
- scoped_events = current_account.events.filtered({
- :trackable => params[:trackable_id],
- :tag => params[:tags],
- :starred => !!params[:starred]
- })
+ # session[:events_view] = {
+ # :controller => 'events',
+ # :page => params[:page],
+ # :per_page => params[:per_page],
+ # :tags => params[:tags],
+ # :trackable_id => params[:trackable_id],
+ # :starred => !!params[:starred]
+ # }
+ scoped_events = current_account.events.filtered(event_params)
@events = scoped_events.paginate(:all, :per_page => (params[:per_page] || 20), :page => params[:page])
respond_to do |format|
View
5 app/helpers/events_helper.rb
@@ -2,9 +2,8 @@ module EventsHelper
def index_description
returning("") do |output|
- if params[:trackable_id]
- @current_project = Trackable.find(params[:trackable_id])
- output << "#{link_to(@current_project, @current_project)} &mdash; "
+ if current_project
+ output << "#{link_to(current_project, current_project)} &mdash; "
end
output << "Events"
View
7 app/models/event/filtering.rb
@@ -6,12 +6,17 @@ def self.included(base)
base.send(:extend, ClassMethods)
base.class_eval do
- named_scope :filtered, lambda { |options| Event.options_for_filter(options) }
+ named_scope :filtered, lambda { |options| Event.filter(options) }
end
end
module ClassMethods
+ def filter(options)
+ options = options.is_a?(Event::Params) ? options : Event::Params.new(options)
+ options.to_finder_options
+ end
+
def keywords_to_options(string)
Event::Params.from_string(string)
end
View
242 app/models/event/params.rb
@@ -4,21 +4,107 @@ class Event::Params
attr_reader :params, :conditions
def initialize(new_params={})
- @params = {}
- @query_options = {}
+ @params = default_params
+ @joins = []
- @conditions = []
+ if new_params.is_a?(String)
+ @search_string = new_params
+ new_params = Event::Params.from_string(new_params)
+ elsif @search_string = new_params.delete(:search)
+ new_params.except!(:search)
+ params_from_search = Event::Params.from_string(@search_string)
+ new_params = params_from_search.merge(new_params)
+ end
new_params.stringify_keys!
- new_params.each do |k,v|
- next if v.blank?
+ @params = normalize_hash(new_params)
+ @params
+ end
+
+ def default_params
+ {
+ :active => true
+ }
+ end
+
+ def to_s
+ params_for_search = @params.dup
+ params_for_search.symbolize_keys!
+
+ search_components = []
+
+ if keywords = params_for_search.delete(:keywords)
+ search_components << keywords #params_for_search[:keywords]
+ end
+
+ if starred = params_for_search.delete(:starred)
+ search_components << "#{string_to_boolean(starred,nil) ? "is" : "not"}:starred"
+ end
+
+ params_for_search.except!(:keywords, :starred)
+
+ params_for_search.each do |key, value|
+ value = %{"#{value}"} if value =~ / /
+ search_components << "#{key}:#{value}"
+ end
+
+ search_components.join(" ")
+ end
+
+ def method_missing(method_name,*args)
+ if method_name.to_s =~ /=$/
+ self[method_name.to_s.gsub(/=/,"")] = args.first
+ else
+ self[method_name.to_s]
+ end
+ end
+
+ def [](key)
+ @params[key.to_s]
+ end
+
+ def []=(key,value)
+ @params.merge!(normalize_hash({key => value}))
+ end
+
+ def klass
+ self.class
+ end
+
+ def normalize_hash(new_params={})
+ new_params.inject({}) do |coll, kv|
+ key, value = kv
- field = k.gsub(klass.operators_regexp, "")
- @params[field] ||= {}
+ if key =~ klass.conditions_regexp
+ coll[key] = value if !value.blank?
+ elsif key =~ /trackable|subject/
+ coll["project"] = value
+ end
- operator = $1.to_s.gsub(/_$/,"")
+ coll
+ end
+ end
+
+ def to_finder_options
+ {
+ :conditions => to_conditions,
+ :joins => (@joins.any? ? @joins.uniq.join(" ") : nil)
+ }
+ end
+
+ def to_conditions
+ @query_options = {}
+
+ @conditions = []
+
+ @params.stringify_keys!
+ @params.each do |k,v|
+ next if v.blank?
- @params[field][operator] = v
+ operator = k.gsub(klass.conditions_regexp, "")
+ field = $1.to_s.gsub(/_$/,"")
+ operator.gsub!(/_/,"")
+ operator = "is" if operator.blank?
@conditions <<
case field.to_s
@@ -27,24 +113,9 @@ def initialize(new_params={})
else
send("condition_for_#{field}", v, operator) if respond_to?("condition_for_#{field}")
end
-
-
-
end
- self
- end
-
- def [](key)
- @params[key]
- end
-
- # def []=(key,value)
- # @params.merge!(klass.from_hash({key => value}))
- # end
-
- def klass
- self.class
+ @conditions.uniq.join(" AND ")
end
def condition_for_timestamp(field,value,operator="")
@@ -52,25 +123,36 @@ def condition_for_timestamp(field,value,operator="")
operator = string_to_comparison_operator(operator)
time = Chronic.parse(value)
- "#{field} #{operator} #{Event.connection.quote time.to_s(:db)}"
+ "#{table_name}.#{field} #{operator} #{quote time.to_s(:db)}"
end
def condition_for_duration(value,operator="")
numeric_value = Event::TimeParser.from_string(value)
operator = string_to_comparison_operator(operator)
- "duration #{operator} #{numeric_value}"
+ "#{table_name}.duration #{operator} #{numeric_value}"
end
def condition_for_keywords(value,operator="")
kc = []
+ phrases = []
blacklisted_words = ('a'..'z').to_a + ["about", "an", "are", "as", "at", "be", "by", "com", "de", "en", "for", "from", "how", "in", "is", "it", "la", "of", "on", "or", "that", "the", "the", "this", "to", "und", "was", "what", "when", "where", "who", "will", "with", "www"]
allowed_characters = 'àáâãäåßéèêëìíîïñòóôõöùúûüýÿ\-_\.@'
- values = value.to_s.gsub(/,;/, " ").split(/ /).uniq
- values.delete_if { |word| word.blank? || blacklisted_words.include?(word.downcase) }
- values.each do |v|
- kc << "body #{operator == "not" ? "NOT LIKE" : "LIKE"} #{Event.connection.quote("%#{v}%")}"
+ # Handling for double quoted strings
+ if value =~ /\"/
+ value.gsub!(/(?:\"(.*?)\"\s*)/) do |match|
+ phrases << $1; ""
+ end
+
+ value.gsub(/ +/," ").strip!
+ end
+
+ # Build SQL query
+ phrases += value.to_s.gsub(/,;/, " ").split(/ /).uniq
+ phrases.delete_if { |word| word.blank? || blacklisted_words.include?(word.downcase) }
+ phrases.each do |v|
+ kc << "#{table_name}.body #{operator == "not" ? "NOT LIKE" : "LIKE"} #{Event.connection.quote("%#{v}%")}"
end
"(#{kc.join(" AND ")})"
@@ -79,7 +161,7 @@ def condition_for_keywords(value,operator="")
def condition_for_tagged(value,operator="")
tc = []
Label.parse(value).each do |tag|
- tc << "tag #{operator == "not" ? "NOT LIKE" : "LIKE"} #{Event.connection.quote("%[#{tag}]%")}"
+ tc << "#{table_name}.tag #{operator == "not" ? "NOT LIKE" : "LIKE"} #{Event.connection.quote("%[#{tag}]%")}"
end
"(#{tc.join(" AND ")})"
@@ -88,11 +170,56 @@ def condition_for_tagged(value,operator="")
def condition_for_date(value,operator="")
date = Chronic.parse(value).to_date
operator = string_to_comparison_operator(operator)
- "date #{operator} #{Event.connection.quote date.to_s(:db)}"
+ "#{table_name}.date #{operator} #{Event.connection.quote date.to_s(:db)}"
end
+ # OPTIMIZE: Expand into a condition_for_boolean
def condition_for_starred(value,operator=nil)
- "starred = #{Event.connection.quote !!value}"
+ value = string_to_boolean(value,operator)
+ "#{table_name}.starred = #{quote !!value}"
+ end
+
+ def condition_for_project(value,operator=nil)
+ if value.to_s =~ /^\d+$/
+ "events.subject_id = #{value}"
+ else
+ @joins << trackable_join
+ "(trackables.name = #{quote value} OR trackables.nickname = #{quote value})"
+ end
+ end
+
+ def condition_for_active(value,operator=nil)
+ @joins << trackable_join
+ value = string_to_boolean(value,operator) ? "active" : "archived"
+ "trackables.state = #{quote value}"
+ end
+
+ def quote(value)
+ Event.connection.quote(value)
+ end
+
+ def trackable_join
+ "LEFT OUTER JOIN trackables ON events.subject_id = trackables.id AND events.account_id = trackables.account_id"
+ end
+
+ def string_to_boolean(value,operator)
+ if [true, false].include?(value)
+ return value
+ end
+
+ value.strip!
+
+ value =
+ if value =~ /^\d$/
+ !!(value.to_i > 0)
+ elsif value =~ /^(?:true|false)$/
+ !!(value == "true")
+ else
+ true
+ end
+
+ value = !value if operator == "not"
+ value
end
def string_to_comparison_operator(operator)
@@ -106,8 +233,20 @@ def string_to_comparison_operator(operator)
end
end
+ def table_name
+ Event.table_name
+ end
+
class << self
+ def available_conditions
+ %w(keywords duration date tagged project created updated starred active)
+ end
+
+ def conditions_regexp
+ /(#{available_conditions.join("|")})/
+ end
+
def operators
%w(from to before after not over under)
end
@@ -158,46 +297,53 @@ def from_string(string)
with.default_keyword :body
with.keyword :body do |values, positive|
- params[:body_has] ||= []
- params[:body_not_have] ||= []
+ params[:keywords] ||= ""
+ params[:not_keywords] ||= ""
values.each do |v|
- params[positive ? :body_has : :body_not_have] << v
+ v = %{"#{v}"} if v =~ / /
+ params[positive ? :keywords : :not_keywords] << "#{v} "
end
end
with.keyword :tagged do |values, positive|
- params[:tag_has] ||= []
- params[:tag_not_have] ||= []
-
values.each do |v|
- params[positive ? :tag_has : :tag_not_have] << "[#{v}]"
+ params[positive ? :tagged : :not_tagged] = v
end
end
with.keyword :project do |values, positive|
- params[:subject_id_is] = []
- params[:subject_id_is_not] = []
-
values.each do |v|
- subj_id = param_for_trackable(v)
- params["subject_id_#{positive ? "is" : "is_not"}".to_sym] << subj_id unless subj_id.nil?
+ params["#{"not_" if !positive}project".to_sym] = v# unless subj_id.nil?
end
end
with.keyword :is do |values|
values.each do |value|
case value.to_s
- when "starred" then params[:starred] = true
+ when "starred" then params[:starred] = "1"
+ when "not_starred" then params[:starred] = "0"
end
end
end
+
+ with.matcher(/^(date|duration|created|updated)/) do |key, values, positive|
+ values.each do |v|
+ params[key.to_sym] = v
+ end
+ end
+
+ with.matcher(/^not/) do |key, values, positive|
+ values.each do |v|
+ params[key.to_sym] = v
+ end
+ end
end
params.inject({}) do |out, kv|
k, v = kv
- out[k] = v if !v.blank?; out
+ out[k] = v.to_s.strip if !v.blank?; out
end
end
View
2  app/views/home/index.html.erb
@@ -5,6 +5,8 @@
<%= flash_message %>
+<%= event_params.to_s %>
+
<div class="ws-frame">
<div class="ws-main">
<% form_tag "/messages", :class => 'bd', :id => "new_event" do %>
View
12 app/views/layouts/_chrome.erb
@@ -34,9 +34,9 @@
</div>
<div class="wr">
+
<div class="wr-c cf">
<div id="left-sidebar">
-
<div class="nav">
<h2><a href="/">TickTock</a></h2>
<ul>
@@ -49,11 +49,17 @@
</li> -->
</ul>
</div>
-
-
</div>
<div id="workspace" class="cf">
+ <div id="search">
+ <form action="/events" method="get">
+ <label>Search your log</label>
+ <input type="text" name="search" value="<%= event_params.to_s %>" />
+ <button type="submit">Search</button>
+ </form>
+ </div>
+
<%= yield %>
</div>
</div>
View
161 test/unit/event/params_test.rb
@@ -1,62 +1,123 @@
require 'test_helper'
-# class Event::Params
-#
-# class Modifier
-# cattr_accessor :modifiers
-# @@modifiers = []
-#
-# attr_reader :name, :aliases
-#
-# def initialize(name,*aliases)
-# @name = name
-# @aliases = aliases
-#
-# self.class.modifiers << self
-# end
-#
-# end
-#
-# end
-
class Event::ParamsTest < ActiveSupport::TestCase
- # supported params:
- # tagged, not_tagged
- # date, date_before, date_after
- # duration, duration_gte, duration_lte, duration_lt, duration_gt
- # is:starred, not:starred
- # has:project, no:project
- # has:tags, no:tags
- # has:duration, no:duration
- # project:, not_project:
- # body_includes, body_excludes
+ context "converting params to search string" do
+ setup do
+ @query_params = {
+ :keywords => '"Yada yada" pork',
+ :tagged => "hello",
+ :project => "GWOD",
+ :date_after => "yesterday"
+ }
+
+ @params = Event::Params.new(@query_params)
+ end
+
+ should "return in proper format" do
+ assert_equal "\"Yada yada\" pork project:GWOD date_after:yesterday tagged:hello", @params.to_s
+ end
+
+ # should "be convertable back to params" do
+ # new_params = Event::Params.from_string(@params.to_s)
+ # assert_equal @query_params, new_params
+ # end
+ end
- # modifiers: has, no, is, not, before|lt, after|gt, on_or_before|lte, on_or_after|gte
+ context "When handling keyword params" do
+ setup {
+ @params = Event::Params.new({ :keywords => "Yada yada" })
+ }
+
+ should "respond to hash-like [] accessor" do
+ assert_equal @params[:keywords], "Yada yada"
+ end
+
+ should "respond to accessor method" do
+ assert_equal @params.keywords, "Yada yada"
+ end
+
+ should "return conditions" do
+ assert_equal "(events.body LIKE '%Yada%' AND events.body LIKE '%yada%')", @params.to_conditions
+ end
+
+ context "containing multiple words" do
+ setup { @params = Event::Params.new({ :keywords => '"Yada yada"' }) }
+
+ should "return conditions" do
+ assert_equal "(events.body LIKE '%Yada yada%')", @params.to_conditions
+ end
+ end
+
+ context "containing multiple words and single words" do
+ setup { @params = Event::Params.new({ :keywords => '"Yada yada" pork' }) }
+
+ should "return conditions" do
+ assert_equal "(events.body LIKE '%Yada yada%' AND events.body LIKE '%pork%')", @params.to_conditions
+ end
+ end
+
+ context "excluding words" do
+ setup { @params = Event::Params.new({ :not_keywords => 'beans' }) }
+
+ should "return conditions" do
+ assert_equal "(events.body NOT LIKE '%beans%')", @params.to_conditions
+ end
+ end
+
+ context "excluding some words but including others" do
+ setup { @params = Event::Params.new({ :not_keywords => 'beans', :keywords => 'pork' }) }
+
+ should "return conditions" do
+ assert_equal "(events.body NOT LIKE '%beans%') AND (events.body LIKE '%pork%')", @params.to_conditions
+ end
+ end
+ end
- # fields: tagged, date, duration, starred, project, body
- # modifiers: (is), not|is_not, includes, excludes, before|lt, after|gt, on_or_before|lte, on_or_after|gte, has, does_not_have
+ context "params containing :starred" do
+ context "set to numeric value" do
+ setup { @params = Event::Params.new({ :starred => "1" }) }
+
+ should "return conditions where starred is true" do
+ assert_equal "events.starred = 't'", @params.to_conditions
+ end
+
+ should "return conditions where starred is false" do
+ @params.starred = "0"
+ assert_equal "events.starred = 'f'", @params.to_conditions
+ end
+ end
+
+ context "set to 'true' or 'false'" do
+ setup { @params = Event::Params.new({ :starred => "true" }) }
+
+ should "return conditions where starred is true" do
+ assert_equal "events.starred = 't'", @params.to_conditions
+ end
+
+ should "return conditions where starred is false" do
+ @params.starred = "false"
+ assert_equal "events.starred = 'f'", @params.to_conditions
+ end
+ end
+ end
- def test_handle_gt
- # Should compile to 'duration > 3600'
- #operators = %w(has is not before less_than after greater_than on_or_before lte on_or_after gte)
-
- params = {
- :duration_from => "1 hour",
- :duration_to => 7200,
- :date_before => "yesterday",
- :tagged => "blah",
- :not_tagged => "yada",
- :keywords => "blah blah",
- :not_keywords => "blah yada",
- :project => "",
- :created_before => "1 hour ago",
- :starred => true
- }
+ context "params containing :not_starred" do
+ setup { @params = Event::Params.new({ :not_starred => "1" }) }
+
+ should "return conditions where starred is false" do
+ assert_equal "events.starred = 'f'", @params.to_conditions
+ end
- o_params = Event::Params.new(params)
+ should "return conditions where starred is true" do
+ @params.not_starred = "0"
+ assert_equal "events.starred = 't'", @params.to_conditions
+ end
- flunk o_params.conditions.inspect
+ should "not return duplicate conditions" do
+ @params.starred = "0"
+ assert_equal "events.starred = 'f'", @params.to_conditions
+ end
end
end
View
34 vendor/keyword_search/definition.rb
@@ -21,6 +21,29 @@ def handle(value, sign)
end
end
+
+ class KeywordMatcher < Keyword
+ attr_reader :regex
+ def initialize(regex, description=nil, &handler)
+ @regex, @description = regex, description
+ @handler = handler
+ end
+
+ def matches?(key)
+ key =~ @regex
+ end
+
+ # Also takes a key argument, since this is more ambiguous
+ def handle(key, value, sign)
+ # If the handler is only expecting one argument,
+ # only give them the positive matches
+ if handler.arity == 2
+ handler.call(key, value) if sign
+ else
+ handler.call(key, value, sign)
+ end
+ end
+ end
def initialize
@default_keyword = nil
@@ -30,10 +53,18 @@ def initialize
def keywords
@keywords ||= []
end
+
+ def matchers
+ @matchers ||= []
+ end
def keyword(name, description=nil, &block)
keywords << Keyword.new(name, description, &block)
end
+
+ def matcher(regex,description=nil,&block)
+ matchers << KeywordMatcher.new(regex, description, &block)
+ end
def default_keyword(name)
@default_keyword = name
@@ -51,6 +82,9 @@ def handle(key, values)
if k = keywords.detect { |kw| kw.name == key.to_sym}
k.handle(true_values, true)
k.handle(false_values, false) if false_values.length > 0
+ elsif k = matchers.detect { |kw| kw.matches?(key.to_s) }
+ k.handle(key.to_s, true_values, true)
+ k.handle(key.to_s, false_values, false) if false_values.length > 0
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.