dokno 1.1.1 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/app/assets/javascripts/dokno.js +48 -37
  4. data/app/assets/stylesheets/dokno/application.css +1 -1
  5. data/app/controllers/dokno/application_controller.rb +1 -1
  6. data/app/controllers/dokno/articles_controller.rb +19 -11
  7. data/app/controllers/dokno/categories_controller.rb +10 -7
  8. data/app/controllers/dokno/pagination_concern.rb +5 -5
  9. data/app/controllers/dokno/user_concern.rb +6 -4
  10. data/app/helpers/dokno/application_helper.rb +1 -1
  11. data/app/models/dokno/application_record.rb +3 -0
  12. data/app/models/dokno/article.rb +91 -42
  13. data/app/models/dokno/category.rb +20 -46
  14. data/app/views/dokno/_article_formatting.html.erb +12 -12
  15. data/app/views/dokno/articles/_article_form.html.erb +49 -10
  16. data/app/views/dokno/articles/show.html.erb +44 -41
  17. data/app/views/dokno/categories/_category_form.html.erb +17 -6
  18. data/app/views/dokno/categories/index.html.erb +54 -41
  19. data/app/views/layouts/dokno/application.html.erb +36 -33
  20. data/app/views/partials/_category_header.html.erb +30 -0
  21. data/app/views/partials/_form_errors.html.erb +0 -1
  22. data/app/views/partials/_logs.html.erb +7 -5
  23. data/app/views/partials/_pagination.html.erb +20 -17
  24. data/config/routes.rb +1 -1
  25. data/db/migrate/20201203190330_baseline.rb +4 -4
  26. data/db/migrate/20201211192306_add_review_due_at_to_articles.rb +6 -0
  27. data/db/migrate/20201213165700_add_starred_to_article.rb +5 -0
  28. data/lib/dokno/config/config.rb +53 -40
  29. data/lib/dokno/engine.rb +5 -5
  30. data/lib/dokno/version.rb +1 -1
  31. data/lib/generators/dokno/templates/config/initializers/dokno.rb +18 -5
  32. metadata +82 -11
@@ -1,26 +1,33 @@
1
+ <% if !current_page?(up_for_review_path) && (@category.blank? || @search_term.present?) %>
2
+ <div class="text-center m-auto mb-10 w-full max-w-screen-xl">
3
+ <% if @search_term.present? %>
4
+ <div class="text-gray-600 text-2xl uppercase">
5
+ <%= @total_records.positive? ? "#{@total_records} #{'article'.pluralize(@total_records)}" : 'No articles' %>
6
+ found containing the search term
7
+ <div class="text-4xl leading-tight"><span class="font-serif">&ldquo;</span> <%= @search_term %> <span class="font-serif">&rdquo;</span> </div>
8
+ </div>
9
+ <% else %>
10
+ <div class="text-gray-600 text-2xl">
11
+ Browse or search
12
+ <% if (article_count = Dokno::Article.count) > 1 %>
13
+ <%= number_with_delimiter(article_count, delimiter: ',') %> articles in
14
+ <% end %>
15
+ the
16
+ </div>
17
+ <div class="text-gray-800 text-4xl leading-tight uppercase"><%= Dokno.config.app_name %> knowledgebase</div>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+
1
22
  <% if @category&.parent.present? %>
2
23
  <div class="text-gray-500 mb-5">
3
- <div><%= @category.breadcrumb %></div>
24
+ <div><span class="text-gray-500 mr-1">Under</span> <%= @category.breadcrumb(search_term: @search_term, order: @order, hide_self: true) %></div>
4
25
  </div>
5
26
  <% end %>
6
27
 
7
- <div class="flex items-center mb-10">
8
- <% if Dokno::Category.exists? %>
9
- <div class="w-1/2 pr-5">
10
-
11
- <select aria-label="Category" name="category" id="category" size="1" class="rounded text-xl shadow-inner bg-gray-100 p-2 w-full max-w-full" onchange="changeCategory(this.value, elem('#search_term').value, '<%= @order %>');">
12
- <option value="">Select a category</option>
13
- <% cache Dokno::Category do %>
14
- <%= Dokno::Category.select_option_markup.html_safe %>
15
- <% end %>
16
- </select>
17
-
18
- </div>
19
- <% end %>
20
- <div class="w-<%= Dokno::Category.exists? ? '1/2 pl-5' : 'full' %>">
21
- <input onsearch="search(this.value, '<%= @order %>');" placeholder="Search article content, titles, and slugs" type="search" name="search_term" id="search_term" value="<%= @search_term %>" class="rounded text-xl shadow-inner bg-gray-100 p-2 w-full" />
22
- </div>
23
- </div>
28
+ <% if !current_page?(up_for_review_path) && (Dokno::Category.exists? || Dokno::Article.exists?) %>
29
+ <%= render 'partials/category_header' %>
30
+ <% end %>
24
31
 
25
32
  <% if @articles.blank? %>
26
33
 
@@ -46,10 +53,10 @@
46
53
  </div>
47
54
  <div class="w-1/3 text-right">
48
55
  <i data-feather="corner-right-down" class="h-5 inline-block" title="Sort order"></i>
49
- <a class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'updated' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=updated">Updated</a>
50
- <a class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'newest' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=newest">Newest</a>
51
- <a class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'views' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=views">Views</a>
52
- <a class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'alpha' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=alpha">Title</a>
56
+ <a id="dokno-order-link-updated" class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'updated' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=updated">Updated</a>
57
+ <a id="dokno-order-link-newest" class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'newest' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=newest">Newest</a>
58
+ <a id="dokno-order-link-views" class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'views' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=views">Views</a>
59
+ <a id="dokno-order-link-alpha" class="ml-3 pb-1 <%= 'border-b-2 border-blue-900' if @order == 'alpha' %>" href="?search_term=<%= CGI.escape @search_term.to_s %>&order=alpha">Title</a>
53
60
  </div>
54
61
  </div>
55
62
 
@@ -58,32 +65,43 @@
58
65
  <section class="border-t border-gray-300 py-10 text-xl flex">
59
66
  <div class="w-1/3 pr-10">
60
67
  <div class="flex">
61
- <div class="no-print w-10 text-gray-300"><i data-feather="chevron-right" class="inline-block"></i></div>
68
+ <div title="<%= 'Starred article' if article.starred %>" class="no-print w-10 text-gray-300"><i data-feather="<%= article.starred ? 'star' : 'chevron-right' %>" class="inline-block"></i></div>
62
69
  <div class="w-full">
63
- <a class="" href="<%= article_path article.slug %>?search_term=<%= @search_term %>" title="View article"><%= article.title %></a>
70
+ <a class="dokno-article-title <% unless article.active %>text-gray-500 italic<% end %>" href="<%= article_path article.slug %>?search_term=<%= @search_term %>&cat_code=<%= @category&.code %>&order=<%= @order %>" title="View article"><%= article.title %></a>
64
71
  </div>
65
72
  </div>
66
73
  </div>
67
74
  <div class="dokno-article-summary w-2/3 <% unless article.active %>text-gray-500 italic<% end %>">
68
- <% unless article.active %>
69
- <div class="bg-yellow-700 p-4 mb-5 rounded text-lg border-t-4 border-yellow-900 text-white font-base not-italic">
70
- <i data-feather="alert-circle" class="inline-block"></i> This article is no longer active
71
- </div>
72
- <% end %>
75
+ <div class="dokno-article-content-highlight mb-2"><%= article.summary.presence || 'No summary provided' %></div>
73
76
 
74
- <span class="dokno-article-content-highlight"><%= article.summary.presence || 'No summary provided' %></span>
77
+ <div class="text-base text-gray-500">
78
+ <%= article.category_name_list(context_category_id: @category&.id, order: @order, search_term: @search_term) %>
79
+ </div>
75
80
 
76
81
  <% unless @order == 'alpha' %>
77
- <div class="text-base mt-2">
82
+ <div class="text-base">
78
83
  <% if @order == 'views' %>
79
- <div class="text-gray-500">Viewed <%= number_with_delimiter(article.views, delimiter: ',') %> <%= 'time'.pluralize(article.views) %></div>
84
+ <div class="text-gray-500">This article was viewed <%= number_with_delimiter(article.views, delimiter: ',') %> <%= 'time'.pluralize(article.views) %></div>
80
85
  <% elsif @order == 'updated' %>
81
- <div class="text-gray-500">Last updated <%= time_ago_in_words article.updated_at %> ago</div>
86
+ <div class="text-gray-500">This article was last updated <%= time_ago_in_words article.updated_at %> ago</div>
82
87
  <% elsif @order == 'newest' %>
83
- <div class="text-gray-500">Added <%= time_ago_in_words article.created_at %> ago</div>
88
+ <div class="text-gray-500">This article was added <%= time_ago_in_words article.created_at %> ago</div>
84
89
  <% end %>
85
90
  </div>
86
91
  <% end %>
92
+
93
+ <% if !article.active %>
94
+ <div class="bg-yellow-700 p-4 mt-5 rounded text-lg border-t-4 border-yellow-900 text-white font-base not-italic">
95
+ <i data-feather="info" class="inline-block mr-1"></i> This article is no longer active
96
+ </div>
97
+ <% end %>
98
+
99
+ <% if article.up_for_review? %>
100
+ <div class="bg-<%= article.review_due_days.negative? ? 'red' : 'gray' %>-800 p-4 mt-5 rounded text-lg border-t-4 border-<%= article.review_due_days.negative? ? 'red' : 'gray' %>-900 text-white font-base not-italic">
101
+ <i data-feather="bell" class="inline-block mr-1"></i> <%= article.review_due_days_string %>
102
+ </div>
103
+ <% end %>
104
+
87
105
  </div>
88
106
  </section>
89
107
  <% end %>
@@ -97,11 +115,6 @@
97
115
  </div>
98
116
  <% end %>
99
117
 
100
- <script>
101
- // Client-side select of cached select list
102
- selectOption('category', '<%= j @category&.code %>');
103
- </script>
104
-
105
- <% if @search_term.present? %>
106
- <script> highlightTerm(['<%= j @search_term.strip %>'], 'dokno-article-content-highlight'); </script>
118
+ <% if @search_term.present? && @articles.blank? %>
119
+ <script> handleSearchHotKey(); </script>
107
120
  <% end %>
@@ -2,6 +2,8 @@
2
2
  <html>
3
3
  <head>
4
4
  <title><%= Dokno.config.app_name %> KNOWLEDGEBASE</title>
5
+
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
7
  <%= csrf_meta_tags %>
6
8
  <%= csp_meta_tag %>
7
9
 
@@ -12,7 +14,8 @@
12
14
  var dokno__base_path = '<%= root_path %>';
13
15
  </script>
14
16
  </head>
15
- <body class="bg-white font-sans font-light subpixel-antialiased">
17
+ <body class="bg-white font-sans font-light subpixel-antialiased text-lg">
18
+
16
19
 
17
20
  <nav id="dokno-nav-container" class="bg-blue-900 text-white py-10 px-16 text-lg">
18
21
  <div class="flex items-center m-auto w-full max-w-screen-xl">
@@ -24,12 +27,12 @@
24
27
  <div class="w-2/3 text-right">
25
28
  <% if can_edit? %>
26
29
  <% if action_name != 'new' %>
27
- <button title="Add a new article" class="bg-gray-700 text-gray-300 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= new_article_path %>/?category_code=<%= @category&.code %>';"><i data-feather="plus-circle" class="inline h-5"></i> ARTICLE</button>
28
- <button title="Add a new category" class="bg-gray-700 text-gray-300 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= new_category_path %>/?parent_category_code=<%= @category&.code %>';"><i data-feather="plus-circle" class="inline h-5"></i> CATEGORY</button>
30
+ <button title="Add a new article" class="bg-gray-700 text-gray-300 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= new_article_path %>/?category_code=<%= @category&.code %>';"><i data-feather="plus" class="inline h-5"></i> ARTICLE</button>
31
+ <button title="Add a new category" class="bg-gray-700 text-gray-300 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= new_category_path %>/?parent_category_code=<%= @category&.code %>';"><i data-feather="plus" class="inline h-5"></i> CATEGORY</button>
29
32
  <% end %>
30
33
 
31
34
  <% if @category&.persisted? && action_name != 'edit' %>
32
- <button title="Edit this category" class="bg-gray-700 text-gray-100 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= edit_category_path(@category) %>';"><i data-feather="edit" class="inline h-5"></i> CATEGORY</button>
35
+ <button title="Edit this category" class="bg-gray-700 text-gray-100 hover:text-white hover:bg-gray-900 rounded ml-3 py-2 px-3 font-bold text-base" onclick="location.href='<%= edit_category_path(@category) %>';"><i data-feather="edit-2" class="inline h-5"></i> CATEGORY</button>
33
36
  <% end %>
34
37
  <% end %>
35
38
 
@@ -40,40 +43,22 @@
40
43
  </div>
41
44
  </nav>
42
45
 
43
- <main class="py-10 px-16">
44
- <% if @article.blank? && (@category.blank? || @search_term.present?) %>
45
- <div class="text-center m-auto mb-10 w-full max-w-screen-xl">
46
- <% if @search_term.present? %>
47
- <div class="text-gray-600 text-2xl uppercase">
48
- <%= @total_records.positive? ? "#{@total_records} #{'article'.pluralize(@total_records)}" : 'No articles' %>
49
- found containing the search term
50
- <div class="text-4xl leading-tight"><span class="font-serif">&ldquo;</span> <%= @search_term %> <span class="font-serif">&rdquo;</span> </div>
51
- </div>
52
- <% else %>
53
- <div class="text-gray-600 text-2xl">
54
- Browse or search
55
- <% if (article_count = Dokno::Article.count) > 1 %>
56
- <%= number_with_delimiter(article_count, delimiter: ',') %> articles in
57
- <% end %>
58
- the
59
- </div>
60
- <div class="text-gray-800 text-4xl leading-tight uppercase"><%= Dokno.config.app_name %> knowledgebase</div>
61
- <% end %>
62
- </div>
63
- <% end %>
64
46
 
65
- <div id="dokno-content-container" class="w-full max-w-screen-xl m-auto print-this">
66
- <% flash.each do |color, msg| %>
67
- <div class="bg-<%= color %>-700 p-4 mb-5 rounded text-lg border-t-4 border-<%= color %>-900 text-white font-base">
68
- <i data-feather="<%= (color == 'green' ? 'check-circle' : (color == 'yellow' ? 'alert-circle' : 'x-circle')) %>" class="inline mr-2"></i>
69
- <%= msg %>
70
- </div>
71
- <% end %>
47
+ <% flash.each do |type, msg| %>
48
+ <div class="bg-<%= type %>-800 text-lg text-white font-base py-10 px-16 text-center">
49
+ <i data-feather="<%= (type == 'green' ? 'smile' : (type == 'yellow' ? 'info' : (type == 'gray' ? 'bell' : 'frown'))) %>" class="inline mr-1"></i>
50
+ <%= sanitize(msg, tags: %w[a], attributes: %w[href class]) %>
51
+ </div>
52
+ <% end %>
72
53
 
54
+
55
+ <main class="py-10 px-16">
56
+ <div id="dokno-content-container" class="w-full max-w-screen-xl m-auto print-this">
73
57
  <%= yield %>
74
58
  </div>
75
59
  </main>
76
60
 
61
+
77
62
  <footer id="dokno-footer-container">
78
63
  <% if @article.present? && action_name == 'show' %>
79
64
  <div id="dokno-article-log-container" data-category-id="<%= @category&.id %>" data-article-id="<%= @article.id %>">
@@ -81,10 +66,24 @@
81
66
  </div>
82
67
  <% end %>
83
68
 
69
+ <% if @show_up_for_review && (up_for_review_count = Dokno::Article.up_for_review.count).positive? %>
70
+ <div id="dokno-articles-up-for-review-container">
71
+ <div class="py-10 px-16 bg-gray-900">
72
+ <div class="w-full max-w-screen-xl m-auto">
73
+ <div class="text-xl text-white cursor-pointer" onclick="location.href='<%= up_for_review_path %>';">
74
+ <i data-feather="bell" class="inline mr-1"></i>
75
+ There <%= "#{up_for_review_count == 1 ? 'is' : 'are'} #{up_for_review_count}" %> <%= 'article'.pluralize(up_for_review_count) %> up for accuracy / relevance review
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
84
82
  <div class="py-10 px-16 text-gray-400 bg-blue-900">
85
83
  <div class="w-full max-w-screen-xl m-auto flex">
86
84
  <div class="w-1/2">
87
- <a target="_blank" href="https://github.com/cpayne624/dokno" title="Knowledgebase">dokno</a>
85
+ <i data-feather="github" class="inline mr-1"></i>
86
+ <a target="_blank" href="https://github.com/cpayne624/dokno" title="Dokno on GitHub">dokno</a>
88
87
  </div>
89
88
  <div class="w-1/2 text-right">
90
89
  <% if user.present? %>
@@ -101,5 +100,9 @@
101
100
  </footer>
102
101
 
103
102
  <%= javascript_include_tag 'init' %>
103
+
104
+ <% if @search_term.present? %>
105
+ <script> highlightTerm(['<%= j @search_term.strip %>'], 'dokno-article-content-highlight'); </script>
106
+ <% end %>
104
107
  </body>
105
108
  </html>
@@ -0,0 +1,30 @@
1
+ <div class="no-print flex items-center mb-10">
2
+ <% if Dokno::Category.exists? %>
3
+ <div class="w-1/2 pr-5">
4
+ <select aria-label="Category" name="category" id="category" size="1" class="rounded text-xl shadow-inner bg-gray-100 p-2 w-full max-w-full" onchange="applyCategoryCriteria(this.value, elem('#search_term').value, '<%= @order %>');">
5
+ <option value="">Uncategorized</option>
6
+
7
+ <optgroup label="Categories">
8
+ <% cache Dokno::Category do %>
9
+ <%= Dokno::Category.select_option_markup.html_safe %>
10
+ <% end %>
11
+ </optgroup>
12
+ </select>
13
+ </div>
14
+
15
+ <script>
16
+ // Client-side select of cached select list
17
+ selectOption('category', '<%= j @category&.code %>');
18
+ </script>
19
+ <% end %>
20
+
21
+ <% if Dokno::Article.exists? %>
22
+ <div class="relative w-<%= Dokno::Category.exists? ? '1/2 pl-5' : 'full' %>">
23
+ <i data-feather="search" class="absolute ml-4 mt-3 inline-block text-gray-300" title="Search"></i>
24
+ <input title="Press / to search" onsearch="applyCategoryCriteria('<%= @category&.code %>', this.value, '<%= @order %>');" onfocus="disableSearchHotkey();" onblur="enableSearchHotkey();" placeholder="Search<%= @category.present? ? ' under this category' : ', hotkey /' %>" type="search" name="search_term" id="search_term" value="<%= @search_term %>" class="pl-12 pr-8 py-2 rounded text-xl shadow-inner bg-gray-100 w-full" />
25
+ <% if @category.present? %><div title="Press / to search" class="absolute -ml-6 mt-2 inline-block text-gray-300 font-semibold">/</div><% end %>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <script> enableSearchHotkey(); </script>
@@ -1,5 +1,4 @@
1
1
  <div id="error_explanation" class="bg-white mb-10">
2
- <i data-feather="alert-octagon" class="float-right"></i>
3
2
  <h2 class="font-semibold">
4
3
  There
5
4
  <% if errors.count == 1 %>
@@ -2,7 +2,7 @@
2
2
  <div class="py-10 px-16 text-gray-200 bg-gray-900">
3
3
  <div class="w-full max-w-screen-xl m-auto">
4
4
  <div class="text-xl text-gray-600 cursor-pointer" onclick="toggleVisibility('change-log');">
5
- Change log for this article
5
+ Change history for this article
6
6
 
7
7
  <div class="inline toggle-visibility-indicator-container change-log">
8
8
  <i data-feather="chevron-left" class="inline toggle-visibility-indicator change-log"></i>
@@ -20,7 +20,7 @@
20
20
  <% end %>
21
21
 
22
22
  <div class="text-gray-500 bg-gray-700 p-5 pr-10 rounded <%= 'cursor-pointer' if log.diff_left != log.diff_right %> flex items-center" onclick="toggleVisibility('article-diff-<%= log.id %>');" title="Show / Hide Diff">
23
- <div class="w-4/5">
23
+ <div class="w-<%= log.diff_left != log.diff_right ? '11/12' : 'full' %>">
24
24
  <%= time_ago_in_words log.created_at %> ago
25
25
  <% if log.username.present? %>
26
26
  by <%= log.username %>
@@ -31,9 +31,11 @@
31
31
  <% end %>
32
32
  </div>
33
33
 
34
- <div class="w-1/5 text-right toggle-visibility-indicator-container article-diff-<%= log.id %>">
35
- <% if log.diff_left != log.diff_right %><i data-feather="chevron-left" class="inline toggle-visibility-indicator article-diff-<%= log.id %>"></i><% end %>
36
- </div>
34
+ <% if log.diff_left != log.diff_right %>
35
+ <div class="w-1/12 text-right toggle-visibility-indicator-container article-diff-<%= log.id %>">
36
+ <i data-feather="chevron-left" class="inline toggle-visibility-indicator article-diff-<%= log.id %>"></i>
37
+ </div>
38
+ <% end %>
37
39
  </div>
38
40
 
39
41
  <% if log.diff_left != log.diff_right %>
@@ -1,28 +1,31 @@
1
- <span class="mr-5">
2
- <% if @page > 1 %>
3
- <span class="mr-1 inline-block"><a href="?search_term=<%= CGI.escape @search_term.to_s %>&order=<%= @order %>&page=<%= (@page - 1) %>"><i data-feather="arrow-left" class="h-5 inline-block" title="Previous page"></i></a></span>
4
- <% end %>
1
+ <% if @total_pages > 1 %>
2
+ <span class="mr-5">
3
+ <% if @page > 1 %>
4
+ <span class="mr-1 inline-block"><a href="?search_term=<%= CGI.escape @search_term.to_s %>&order=<%= @order %>&page=<%= (@page - 1) %>"><i data-feather="arrow-left" class="h-5 inline-block" title="Previous page"></i></a></span>
5
+ <% end %>
5
6
 
6
- <span class="mr-1 inline-block">Page</span>
7
+ <span class="mr-1 inline-block">Page</span>
7
8
 
8
- <%= form_with(url: article_index_path(@category&.code), method: :get, class: 'inline') do %>
9
- <input type="hidden" name="search_term" value="<%= @search_term %>">
10
- <input type="hidden" name="order" value="<%= @order %>">
11
- <input aria-label="Page" type="text" name="page" value="<%= @page %>" onclick="this.select();" class="w-10 text-center bg-gray-200 rounded" />
9
+ <%= form_with(url: article_index_path(@category&.code), method: :get, class: 'inline') do %>
10
+ <input type="hidden" name="search_term" value="<%= @search_term %>">
11
+ <input type="hidden" name="order" value="<%= @order %>">
12
+ <input aria-label="Page" type="text" name="page" value="<%= @page %>" onclick="this.select();" class="w-10 text-center bg-gray-200 rounded" />
12
13
 
13
- <span class="mx-1 inline-block">of</span>
14
- <span class="text-center inline-block"><%= @total_pages %></span>
15
- <% end %>
14
+ <span class="mx-1 inline-block">of</span>
15
+ <span class="text-center inline-block"><%= @total_pages %></span>
16
+ <% end %>
16
17
 
17
- <% if @page < @total_pages %>
18
- <span class="ml-1 inline-block"><a href="?search_term=<%= CGI.escape @search_term.to_s %>&order=<%= @order %>&page=<%= (@page + 1) %>"><i data-feather="arrow-right" class="h-5 inline-block" title="Next page"></i></a></span>
19
- <% end %>
20
- </span>
18
+ <% if @page < @total_pages %>
19
+ <span class="ml-1 inline-block"><a href="?search_term=<%= CGI.escape @search_term.to_s %>&order=<%= @order %>&page=<%= (@page + 1) %>"><i data-feather="arrow-right" class="h-5 inline-block" title="Next page"></i></a></span>
20
+ <% end %>
21
+ </span>
22
+ <% end %>
21
23
 
22
24
  <span class="text-gray-400">
23
25
  <%= @total_records %>
24
- <%= 'uncategorized' if @category.blank? && @search_term.blank? %>
26
+ <%= 'uncategorized' if !current_page?(up_for_review_path) && @category.blank? && @search_term.blank? %>
25
27
  <%= 'article'.pluralize(@total_records) %>
28
+ <%= 'up for review' if current_page?(up_for_review_path) %>
26
29
 
27
30
  <% if @search_term.present? %>
28
31
  containing <span class="font-serif">&ldquo;</span><%= @search_term %><span class="font-serif">&rdquo;</span>
data/config/routes.rb CHANGED
@@ -3,8 +3,8 @@ Dokno::Engine.routes.draw do
3
3
  resources :articles
4
4
 
5
5
  get '/(:cat_code)', to: 'categories#index', as: :article_index
6
+ get '/up_for_review', to: 'categories#index', as: :up_for_review
6
7
  get 'article_panel/(:slug)', to: 'articles#panel', as: :panel
7
- post 'article_log', to: 'articles#article_log', as: :article_log
8
8
  post 'article_preview', to: 'articles#preview', as: :preview
9
9
  post 'article_status', to: 'articles#status', as: :status
10
10
  root 'categories#index'
@@ -13,12 +13,12 @@ class Baseline < ActiveRecord::Migration[6.0]
13
13
  t.string "slug"
14
14
  t.string "title"
15
15
  t.text "markdown"
16
- t.datetime "created_at", precision: 6, null: false
17
- t.datetime "updated_at", precision: 6, null: false
18
16
  t.text "summary"
19
17
  t.boolean "active", default: true
20
18
  t.bigint "views", default: 0
21
19
  t.datetime "last_viewed_at"
20
+ t.datetime "created_at", precision: 6, null: false
21
+ t.datetime "updated_at", precision: 6, null: false
22
22
  t.index ["slug"], name: "index_dokno_articles_on_slug", unique: true
23
23
  end
24
24
 
@@ -32,10 +32,10 @@ class Baseline < ActiveRecord::Migration[6.0]
32
32
 
33
33
  create_table "dokno_categories", force: :cascade do |t|
34
34
  t.string "name"
35
- t.datetime "created_at", precision: 6, null: false
36
- t.datetime "updated_at", precision: 6, null: false
37
35
  t.bigint "category_id"
38
36
  t.string "code", null: false
37
+ t.datetime "created_at", precision: 6, null: false
38
+ t.datetime "updated_at", precision: 6, null: false
39
39
  t.index ["category_id"], name: "index_dokno_categories_on_category_id"
40
40
  t.index ["code"], name: "index_dokno_categories_on_code", unique: true
41
41
  t.index ["name"], name: "index_dokno_categories_on_name", unique: true
@@ -0,0 +1,6 @@
1
+ class AddReviewDueAtToArticles < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :dokno_articles, :review_due_at, :datetime
4
+ add_index :dokno_articles, :review_due_at
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddStarredToArticle < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :dokno_articles, :starred, :boolean, default: false
4
+ end
5
+ end
@@ -1,4 +1,8 @@
1
1
  module Dokno
2
+ module Error
3
+ class Config < StandardError; end
4
+ end
5
+
2
6
  def self.configure
3
7
  yield config
4
8
  config.validate
@@ -8,62 +12,71 @@ module Dokno
8
12
  @config ||= Config.new
9
13
  end
10
14
 
11
- def self.config=(val)
12
- @config = val
13
- end
14
-
15
15
  class Config
16
- # (String) Host application name for display within the mounted dashboard
17
- attr_accessor :app_name
16
+ # Dokno configuration options
17
+ #
18
+ # app_name (String)
19
+ # Host app name for display within the mounted dashboard
20
+ # tag_whitelist (Enumerable)
21
+ # Determines which HTML tags are allowed in Article markdown
22
+ # attr_whitelist (Enumerable)
23
+ # Determines which HTML attributes are allowed in Article markdown
24
+ # app_user_object (String)
25
+ # Host app's user object
26
+ # app_user_auth_method (Symbol)
27
+ # Host app's user object method to be used for edit authorization.
28
+ # Should return boolean
29
+ # app_user_name_method (Symbol)
30
+ # Host app's user object method that returns the authenticated user's name or other
31
+ # identifier that will be included in change log events.
32
+ # Should return a string
33
+ # article_review_period (ActiveSupport::Duration)
34
+ # The amount of time before articles should be reviewed for accuracy/relevance
35
+ # article_review_prompt_days (Integer)
36
+ # The number of days prior to an article being up for review that users should be prompted
18
37
 
19
- # (Enumerable) Determines which HTML tags are allowed in Article markdown
38
+ attr_accessor :app_name
20
39
  attr_accessor :tag_whitelist
21
-
22
- # (Enumerable) Determines which HTML attributes are allowed in Article markdown
23
40
  attr_accessor :attr_whitelist
24
-
25
- # (String) Host application's user object
26
41
  attr_accessor :app_user_object
27
-
28
- # (Symbol) Host application's user object method that should be used to authorize users to edit Dokno data
29
- # Should return boolean.
30
42
  attr_accessor :app_user_auth_method
31
-
32
- # (Symbol) Host application's user object method that should return the authenticated user's name or other
33
- # identifier that will be included in change log events. Should return a string.
34
43
  attr_accessor :app_user_name_method
44
+ attr_accessor :article_review_period
45
+ attr_accessor :article_review_prompt_days
35
46
 
36
47
  # Defaults
37
- TAG_WHITELIST = %w[code img h1 h2 h3 h4 h5 h6 a em u i b strong ol ul li table thead tbody tfoot tr th td blockquote hr br p]
38
- ATTR_WHITELIST = %w[src alt title href target]
39
- APP_USER_OBJECT = 'current_user'
40
- APP_USER_AUTH_METHOD = 'admin?'
41
- APP_USER_NAME_METHOD = 'name'
48
+ TAG_WHITELIST = %w[code img h1 h2 h3 h4 h5 h6 a em u i b strong ol ul li table thead tbody tfoot tr th td blockquote hr br p]
49
+ ATTR_WHITELIST = %w[src alt title href target]
50
+ APP_USER_OBJECT = 'current_user'
51
+ APP_USER_AUTH_METHOD = :admin?
52
+ APP_USER_NAME_METHOD = :name
53
+ ARTICLE_REVIEW_PERIOD = 1.year
54
+ ARTICLE_REVIEW_PROMPT_DAYS = 30
42
55
 
43
56
  def initialize
44
- self.app_name = Rails.application.class.module_parent.name.underscore.humanize.upcase
45
- self.tag_whitelist = TAG_WHITELIST
46
- self.attr_whitelist = ATTR_WHITELIST
47
- self.app_user_object = APP_USER_OBJECT
48
- self.app_user_auth_method = APP_USER_AUTH_METHOD
49
- self.app_user_name_method = APP_USER_NAME_METHOD
57
+ self.app_name = Rails.application.class.module_parent.name.underscore.humanize.upcase
58
+ self.tag_whitelist = TAG_WHITELIST
59
+ self.attr_whitelist = ATTR_WHITELIST
60
+ self.app_user_object = APP_USER_OBJECT
61
+ self.app_user_auth_method = APP_USER_AUTH_METHOD
62
+ self.app_user_name_method = APP_USER_NAME_METHOD
63
+ self.article_review_period = ARTICLE_REVIEW_PERIOD
64
+ self.article_review_prompt_days = ARTICLE_REVIEW_PROMPT_DAYS
50
65
  end
51
66
 
52
67
  def validate
53
- validate_tag_whitelist
54
- validate_attr_whitelist
68
+ validate_config_option(option: 'tag_whitelist', expected_class: Enumerable, example: '%w[a p strong]')
69
+ validate_config_option(option: 'attr_whitelist', expected_class: Enumerable, example: '%w[class href]')
70
+ validate_config_option(option: 'app_user_object', expected_class: String, example: 'current_user')
71
+ validate_config_option(option: 'app_user_auth_method', expected_class: Symbol, example: ':admin?')
72
+ validate_config_option(option: 'app_user_name_method', expected_class: Symbol, example: ':name')
73
+ validate_config_option(option: 'article_review_period', expected_class: ActiveSupport::Duration, example: '1.year')
74
+ validate_config_option(option: 'article_review_prompt_days', expected_class: Integer, example: '30')
55
75
  end
56
76
 
57
- def validate_tag_whitelist
58
- return unless !tag_whitelist.is_a?(Enumerable)
59
-
60
- raise "#{config_error_prefix} tag_whitelist must be Enumerable"
61
- end
62
-
63
- def validate_attr_whitelist
64
- return unless !attr_whitelist.is_a?(Enumerable)
65
-
66
- raise "#{config_error_prefix} attr_whitelist must be Enumerable"
77
+ def validate_config_option(option:, expected_class:, example:)
78
+ return unless !send(option.to_sym).is_a? expected_class
79
+ raise Error::Config, "#{config_error_prefix} #{option} must be #{expected_class}, e.g. #{example}"
67
80
  end
68
81
 
69
82
  def config_error_prefix