5 コミット

作成者 SHA1 メッセージ 日付
  Dylan Baker 59bb48b2ef Fix next_page heuristic 3年前
  Dylan Baker 8d3e2a9c08 Fix result limit/offset mismatch 3年前
  Dylan Baker 8a1b934bac Don't show next page button if there is no next page 3年前
  Dylan Baker b3aafafa29 Implement date filters 3年前
  Dylan Baker bfc8cd2511 Log in development 4年前
7個のファイルの変更219行の追加105行の削除
  1. 1
    0
      .gitignore
  2. 3
    1
      db/connect.rb
  3. 55
    16
      lib/search.rb
  4. 11
    1
      web/assets/styles/form.scss
  5. 34
    11
      web/server.rb
  6. 52
    22
      web/views/partials/search.erb
  7. 63
    54
      web/views/results.erb

+ 1
- 0
.gitignore ファイルの表示

@@ -2,3 +2,4 @@
2 2
 vendor/
3 3
 web/public/*.css
4 4
 web/public/*.js
5
+db/log

+ 3
- 1
db/connect.rb ファイルの表示

@@ -1,4 +1,5 @@
1 1
 require 'dotenv'
2
+require 'logger'
2 3
 
3 4
 Dotenv.load(File.expand_path('../.env'))
4 5
 
@@ -7,5 +8,6 @@ DB =
7 8
     adapter: :postgres,
8 9
     database: ENV['DB_DATABASE'],
9 10
     user: ENV['DB_USERNAME'],
10
-    password: ENV['DB_PASSWORD']
11
+    password: ENV['DB_PASSWORD'],
12
+    logger: ENV['APP_ENV'] == 'development' ? Logger.new('db/log') : nil
11 13
   )

+ 55
- 16
lib/search.rb ファイルの表示

@@ -1,10 +1,43 @@
1 1
 require 'sequel'
2 2
 
3
+RESULTS_PER_PAGE = 50
4
+
3 5
 def search(params)
4 6
   query = params[:q].strip
5
-  offset = (params[:page] - 1) * 10
7
+  offset = (params[:page] - 1) * RESULTS_PER_PAGE
6 8
   username = params[:username].strip
7
-  case params[:type]
9
+  from_date = params[:from_date].strip
10
+  to_date = params[:to_date].strip
11
+
12
+  errors = Array.new
13
+
14
+  if from_date.empty?
15
+    from_date = nil
16
+  else
17
+    if from_date.match(/\d{4}-\d{2}-\d{2}/)
18
+      from_date = Date.parse(params[:from_date]) rescue errors << 'Invalid From Date'
19
+    else
20
+      errors << 'From Date must be in the format YYYY-MM-DD'
21
+    end
22
+  end
23
+
24
+  if to_date.empty?
25
+    to_date = nil
26
+  else
27
+    if to_date.match(/\d{4}-\d{2}-\d{2}/)
28
+      to_date = Date.parse(params[:to_date]).next rescue errors << 'Invalid To Date'
29
+    else
30
+      errors << 'To Date must be in the format YYYY-MM-DD'
31
+    end
32
+  end
33
+
34
+  if errors.empty? && !to_date.nil? && !from_date.nil? &&  to_date < from_date
35
+    errors << 'To Date must be after From Date'
36
+  end
37
+
38
+  return {results: Array.new, errors: errors} unless errors.empty?
39
+
40
+  results = case params[:type]
8 41
   when 'threads'
9 42
     sort =
10 43
       case params[:sort]
@@ -15,43 +48,49 @@ def search(params)
15 48
       else
16 49
         'created_at DESC'
17 50
       end
18
-    search_threads(query, username, sort, offset)
51
+    search_threads(query, username, from_date, to_date, sort, offset)
19 52
   when 'posts'
20
-    search_posts(query, username, offset)
53
+    search_posts(query, username, from_date, to_date, offset)
21 54
   else
22 55
     Array.new
23 56
   end
57
+
58
+  {results: results, errors: errors}
24 59
 end
25 60
 
26
-def search_threads(query, username, sort, offset)
27
-  DB[<<-SQL, query, username, username, offset]
61
+def search_threads(query, username, from_date, to_date, sort, offset)
62
+  DB[<<-SQL, query, username, username, from_date, from_date, to_date, to_date, offset]
28 63
     SELECT
29
-      threads.*
64
+      threads.*,
65
+      count(*) OVER() AS full_count
30 66
     FROM threads
31 67
     WHERE
32 68
       to_tsvector(title) @@ plainto_tsquery(?)
33 69
       AND (LOWER(threads.creator) = LOWER(?) OR ? = '')
70
+      AND (created_at >= ? OR ? IS NULL)
71
+      AND (created_at <= ? OR ? IS NULL)
34 72
     ORDER BY #{sort}
35
-    LIMIT 50
73
+    LIMIT #{RESULTS_PER_PAGE}
36 74
     OFFSET ?;
37 75
   SQL
38 76
 end
39 77
 
40
-def search_posts(query, username, offset)
41
-  DB[<<-SQL, query, username, username, offset]
78
+def search_posts(query, username, from_date, to_date, offset)
79
+  DB[<<-SQL, query, username, username, from_date, from_date, to_date, to_date, offset]
42 80
     SELECT
43 81
       posts.*,
44 82
       threads.title as thread_title,
45
-      threads.remote_id as remote_thread_id
83
+      threads.remote_id as remote_thread_id,
84
+      count(*) OVER() AS full_count
46 85
     FROM posts
47 86
     INNER JOIN threads on posts.thread_id = threads.id
48 87
     WHERE
49 88
       tsv @@ plainto_tsquery(?)
50
-      AND (
51
-        (LOWER(posts.creator) = LOWER(?)) OR (? = '')
52
-      )
53
-    ORDER BY created_at DESC
54
-    LIMIT 50
89
+      AND ((LOWER(posts.creator) = LOWER(?)) OR (? = ''))
90
+      AND (posts.created_at >= ? OR ? IS NULL)
91
+      AND (posts.created_at <= ? OR ? IS NULL)
92
+    ORDER BY posts.created_at DESC
93
+    LIMIT #{RESULTS_PER_PAGE}
55 94
     OFFSET ?;
56 95
   SQL
57 96
 end

+ 11
- 1
web/assets/styles/form.scss ファイルの表示

@@ -17,12 +17,22 @@
17 17
   margin: 1em 0;
18 18
 }
19 19
 
20
+.filters__section {
21
+  display: flex;
22
+  padding: .5em 0;
23
+}
24
+
20 25
 .filters__section--sort {
21 26
   display: none;
22 27
 }
23 28
 
24 29
 .filters__section--sort.open {
25
-  display: block;
30
+  display: flex;
31
+}
32
+
33
+.filters__subsection {
34
+  flex: 1;
35
+  padding: 0 1em;
26 36
 }
27 37
 
28 38
 .form__field {

+ 34
- 11
web/server.rb ファイルの表示

@@ -34,16 +34,28 @@ class VLVSearch < Sinatra::Base
34 34
     params[:page] = params[:page].to_i
35 35
     params[:q] = String.new unless params[:q]
36 36
     params[:username] = String.new unless params[:username]
37
+    params[:from_date] = String.new unless params[:from_date]
38
+    params[:to_date] = String.new unless params[:to_date]
37 39
 
38 40
     results = search(params)
39
-    previous_url, next_url = build_urls(params)
40 41
 
41
-    locals =
42
-      params.merge(
43
-        results: results, previous_url: previous_url, next_url: next_url
44
-      )
42
+    unless results[:errors].empty?
43
+      erb :results, { locals: {errors: results[:errors]}, layout: :layout }
44
+    else
45
+      params[:current_count] = results[:results].to_a.size
46
+      params[:full_count] = results[:results].empty? ? 0 : results[:results].first[:full_count]
47
+      previous_url, next_url = build_urls(params)
48
+
49
+      locals =
50
+        params.merge(
51
+          results: results[:results],
52
+          previous_url: previous_url,
53
+          next_url: next_url,
54
+          errors: Array.new,
55
+        )
45 56
 
46
-    erb :results, { locals: locals, layout: :layout }
57
+      erb :results, { locals: locals, layout: :layout }
58
+    end
47 59
   end
48 60
 
49 61
   get '/login' do
@@ -75,17 +87,28 @@ class VLVSearch < Sinatra::Base
75 87
     def build_urls(params)
76 88
       current_page = params[:page].to_i
77 89
       previous_page = current_page > 1 ? current_page - 1 : nil
78
-      next_page = current_page + 1
90
+
91
+      if (params[:current_count] + ((current_page - 1) * RESULTS_PER_PAGE)) == params[:full_count]
92
+        next_page = nil
93
+      else
94
+        next_page = current_page + 1
95
+      end
79 96
 
80 97
       url_params = { q: params[:q], type: params[:type] }
81 98
       url_params[:username] = params[:username] if params[:username]
99
+      url_params[:from_date] = params[:from_date] if params[:from_date]
100
+      url_params[:to_date] = params[:to_date] if params[:to_date]
82 101
 
83
-      [previous_page, next_page].map do |page|
102
+      [
84 103
         URI::Generic.build(
85 104
           path: '/search',
86
-          query: URI.encode_www_form(url_params.merge(page: page))
87
-        )
88
-      end
105
+          query: URI.encode_www_form(url_params.merge(page: previous_page))
106
+        ),
107
+        next_page.nil? ? nil : URI::Generic.build(
108
+          path: '/search',
109
+          query: URI.encode_www_form(url_params.merge(page: next_page))
110
+        ),
111
+      ]
89 112
     end
90 113
 
91 114
     def current_user

+ 52
- 22
web/views/partials/search.erb ファイルの表示

@@ -2,30 +2,60 @@
2 2
   <input name="q" type="search" value="<%= params[:q] %>" placeholder="Search for..." class="form__search" required>
3 3
   <div class="filters">
4 4
     <div class="filters__section">
5
-      <p>Search in:</p>
6
-      <label class="form__label">
7
-        <input type="radio" name="type" value="threads" <% if [nil, "threads"].include?(params[:type]) %>checked<% end%>>
8
-        Threads
9
-      </label>
10
-      <label class="form__label">
11
-        <input type="radio" name="type" value="posts" <% if params[:type] == "posts" %>checked<% end %>>
12
-        Posts
13
-      </label>
5
+      <div class="filters__subsection">
6
+        <p>Search in:</p>
7
+        <label class="form__label">
8
+          <input type="radio" name="type" value="threads" <% if [nil, "threads"].include?(params[:type]) %>checked<% end%>>
9
+          Threads
10
+        </label>
11
+        <label class="form__label">
12
+          <input type="radio" name="type" value="posts" <% if params[:type] == "posts" %>checked<% end %>>
13
+          Posts
14
+        </label>
15
+      </div>
16
+      <div class="filters__subsection">
17
+        <p>From Date <em>(YYYY-MM-DD)</em>:</p>
18
+        <label class="form__label">
19
+          <input
20
+          type="text"
21
+          name="from_date"
22
+          value="<%= params[:from_date] %>"
23
+          class="form__text-field"
24
+          pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"
25
+          maxlength="10"
26
+        >
27
+        </label>
28
+      </div>
29
+      <div class="filters__subsection">
30
+        <p>To Date <em>(YYYY-MM-DD)</em>:</p>
31
+        <label class="form__label">
32
+          <input
33
+          type="text"
34
+          name="to_date"
35
+          value="<%= params[:to_date] %>"
36
+          class="form__text-field"
37
+          pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"
38
+          maxlength="10"
39
+        >
40
+        </label>
41
+      </div>
14 42
     </div>
15 43
     <div class="filters__section filters__section--sort <%= params[:type] == 'threads' ? 'open' : '' %>">
16
-      <p>Sort by:</p>
17
-      <label class="form__label">
18
-        <input type="radio" name="sort" value="thread" <% if [nil, "thread"].include? params[:sort] %>checked<% end%>>
19
-        Thread creation
20
-      </label>
21
-      <label class="form__label">
22
-        <input type="radio" name="sort" value="post" <% if params[:sort] == "post" %>checked<% end %>>
23
-        Most recent post
24
-      </label>
25
-    </div>
26
-    <div class="filters__section">
27
-      <p>Posted by:</p>
28
-      <input type="text" name="username" value="<%= params[:username] %>" class="form__text-field">
44
+      <div class="filters__subsection">
45
+        <p>Sort by:</p>
46
+        <label class="form__label">
47
+          <input type="radio" name="sort" value="thread" <% if [nil, "thread"].include? params[:sort] %>checked<% end%>>
48
+          Thread creation
49
+        </label>
50
+        <label class="form__label">
51
+          <input type="radio" name="sort" value="post" <% if params[:sort] == "post" %>checked<% end %>>
52
+          Most recent post
53
+        </label>
54
+      </div>
55
+      <div class="filters__subsection">
56
+        <p>Posted by:</p>
57
+        <input type="text" name="username" value="<%= params[:username] %>" class="form__text-field">
58
+      </div>
29 59
     </div>
30 60
   </div>
31 61
   <input type="submit" value="Search" class="btn form__submit">

+ 63
- 54
web/views/results.erb ファイルの表示

@@ -1,63 +1,72 @@
1 1
 <%= erb :'partials/search' %>
2 2
 
3 3
 <div class="results-container">
4
-  <% if params[:q] %>
5
-    <p>Showing results for <strong><em><%= params[:q] %></em></strong>:</p>
6
-  <% end %>
7
-  <div class="results">
8
-    <% if results.empty? %>
9
-      <h3>No results!</h3>
4
+  <% if errors.empty? %>
5
+    <% if params[:q] %>
6
+      <p>Showing results for <strong><em><%= params[:q] %></em></strong>:</p>
10 7
     <% end %>
11
-    <% results.each do |result| %>
12
-      <div class="results__result <%= params[:type] %>">
13
-        <% if params[:type] == "threads" %>
14
-          <%= external_link(
15
-            "http://board.vivalavinyl.com/thread/view/#{result[:remote_id]}",
16
-            result[:title]
17
-          ) %>
18
-          <p class="result__info">
19
-            <span>
20
-              created by <%= result[:creator] %>
21
-              on <%= result[:created_at].strftime('%B %d, %Y') %>
22
-            </span>
23
-            <span>
24
-              last post by <%= result[:last_post_creator] %>
25
-              at <%= result[:last_post_created_at].strftime('%-I:%M %p EST') %>
26
-              on <%= result[:last_post_created_at].strftime('%B %-d, %Y') %>
27
-            </span>
28
-          </p>
29
-        <% elsif params[:type] == "posts" %>
30
-          <p>
31
-            In thread:
32
-            <strong>
33
-              <%= external_link(
34
-                "http://board.vivalavinyl.com/thread/view/#{result[:remote_thread_id]}",
35
-                result[:thread_title]
36
-              ) %>
37
-            </strong>
38
-          </p>
39
-          <p class="result__info">
40
-            <%= result[:creator] %> posted this at <%= result[:created_at] %>
41
-          </p>
42
-          <p class="result__body">
43
-            <%= result[:body] %>
44
-          </p>
8
+    <div class="results">
9
+      <% if results.empty? %>
10
+        <h3>No results!</h3>
11
+      <% end %>
12
+      <% results.each do |result| %>
13
+        <div class="results__result <%= params[:type] %>">
14
+          <% if params[:type] == "threads" %>
15
+            <%= external_link(
16
+              "http://board.vivalavinyl.com/thread/view/#{result[:remote_id]}",
17
+              result[:title]
18
+            ) %>
19
+            <p class="result__info">
20
+              <span>
21
+                created by <%= result[:creator] %>
22
+                on <%= result[:created_at].strftime('%B %d, %Y') %>
23
+              </span>
24
+              <span>
25
+                last post by <%= result[:last_post_creator] %>
26
+                at <%= result[:last_post_created_at].strftime('%-I:%M %p EST') %>
27
+                on <%= result[:last_post_created_at].strftime('%B %-d, %Y') %>
28
+              </span>
29
+            </p>
30
+          <% elsif params[:type] == "posts" %>
31
+            <p>
32
+              In thread:
33
+              <strong>
34
+                <%= external_link(
35
+                  "http://board.vivalavinyl.com/thread/view/#{result[:remote_thread_id]}",
36
+                  result[:thread_title]
37
+                ) %>
38
+              </strong>
39
+            </p>
40
+            <p class="result__info">
41
+              <%= result[:creator] %> posted this at <%= result[:created_at] %>
42
+            </p>
43
+            <p class="result__body">
44
+              <%= result[:body] %>
45
+            </p>
46
+          <% end %>
47
+        </div>
48
+      <% end %>
49
+
50
+      <% if params[:q] %>
51
+        <% if params[:page] > 1 %>
52
+          <a class="btn btn--small btn--prev" href="<%= previous_url %>">
53
+            previous page
54
+          </a>
45 55
         <% end %>
46
-      </div>
47
-    <% end %>
48 56
 
49
-    <% if params[:q] %>
50
-      <% if params[:page] > 1 %>
51
-        <a class="btn btn--small btn--prev" href="<%= previous_url %>">
52
-          previous page
53
-        </a>
57
+        <% unless results.empty? || next_url.nil? %>
58
+          <a class="btn btn--small btn--next" href="<%= next_url %>">
59
+            next page
60
+          </a>
61
+        <% end %>
54 62
       <% end %>
55
-
56
-      <% unless results.empty? %>
57
-        <a class="btn btn--small btn--next" href="<%= next_url %>">
58
-          next page
59
-        </a>
63
+    </div>
64
+  <% else %>
65
+    <div class="errors">
66
+      <h3 class="errors__heading">Error(s):</h3>
67
+      <% errors.each do |error| %>
68
+        <p class="errors__error error"><%= error %></p>
60 69
       <% end %>
61
-    <% end %>
62
-  </div>
70
+    </div>
71
+  <% end %>
63 72
 </div>

読み込み中…
キャンセル
保存