dokno 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1032443316f9af5efc5d9d858fc72977e246c3dc04bf411818b6313cd9eb274a
4
- data.tar.gz: a079f1d09677c56750b2c9b0a9f438505a60d4727cfa9bc1e036b6ef94783dcc
3
+ metadata.gz: edd6bc7f282e312599b5d844e48382a5eff63604c13b05d41859c1beb587e878
4
+ data.tar.gz: b692a98a50654ae4d7d7a77b123289da686ab37131a0bbdf0f4ffc9151b5a1ab
5
5
  SHA512:
6
- metadata.gz: b58630386a83f001e2e3a49def7e14cb833de066724be74b44ca56b08494ff9a72994ea0c2e5f324b09832a2f8848921fd9fa1eb1a41e83f11e8aba166669438
7
- data.tar.gz: 6f1cf01dcd513f4dc4ce57acc85ba7dcb42aaac06585431562bdf06c5f9c012e0ea8a0c7588974045ca1e14704e9d2630a68150c000a032e321bf0c70a61f0bd
6
+ metadata.gz: ce6b652432db90585a4b72fe2327a5201409d9872c3408b74d7e5a52253d960b2ce1286fad4c762e09f2d4b72134d49174c4bbe3d4fc02203de1b3d95cc8077c
7
+ data.tar.gz: ae70e70feb6f78cc30daf751085ef58f50a26b45c9faa1276b5723fdb685c08ada77ff02ba5dee1a781a90c3852d0822702e7a410a2e952baef1545ded3408b2
data/README.md CHANGED
@@ -36,11 +36,11 @@ To enable [in-context articles](#in-context-article-links) in your app, add the
36
36
  <%= render 'dokno/article_panel' %>
37
37
  ```
38
38
 
39
- ## Screenshots
39
+ ### Configuration
40
40
 
41
- | Landing Page | Article | Editing an Article | Article Flyout |
42
- | ------------- | ------------- | ------------- | ------------- |
43
- | <img src="./README/landing_page.png" width="250"> | <img src="./README/article.png" width="250"> | <img src="./README/article_edit.png" width="250"> | <img src="./README/host_app_flyout.png" width="250"> |
41
+ #### Dokno Settings
42
+
43
+ Running `rails g dokno:install` creates `/config/initializers/dokno.rb` within your app, containing the available Dokno configuration options. Remember to restart your app whenever you make configuration changes.
44
44
 
45
45
  ### Articles
46
46
 
@@ -72,8 +72,6 @@ Clicking a link fetches the article asynchronously and reveals it to the user vi
72
72
 
73
73
  <%= dokno_article_link({link-text}, slug: {unique-article-slug}) %>
74
74
 
75
- <img src="./README/host_app_flyout.png" width="50%">
76
-
77
75
  ### Dokno Data Querying
78
76
  You typically won't need to interact with Dokno data directly, but it is stored within your database and is accessible via ActiveRecord as is any other model.
79
77
 
@@ -175,3 +175,22 @@ function highlightTerm(terms, containers_selector) {
175
175
  function wrapTermWithHTML(term) {
176
176
  return `<span title="Matching search term" class="dokno-search-term bg-yellow-300 text-gray-900 p-2 rounded mx-1">${term}</span>`
177
177
  }
178
+
179
+ function setReviewForm() {
180
+ const reset_review_date_checkbox = elem('input#reset_review_date');
181
+ const review_notes_textarea = elem('textarea#review_notes');
182
+
183
+ if (!reset_review_date_checkbox) {
184
+ return true;
185
+ }
186
+
187
+ if (reset_review_date_checkbox.checked) {
188
+ review_notes_textarea.removeAttribute('disabled');
189
+ review_notes_textarea.classList.remove('cursor-not-allowed');
190
+ review_notes_textarea.focus();
191
+ } else {
192
+ review_notes_textarea.setAttribute('disabled', 'disabled');
193
+ review_notes_textarea.classList.add('cursor-not-allowed');
194
+ reset_review_date_checkbox.focus();
195
+ }
196
+ }
@@ -16,7 +16,7 @@
16
16
 
17
17
  /* Additional knowledgebase site styles that are not in tailwind */
18
18
 
19
- svg.feather {
19
+ button svg.feather {
20
20
  vertical-align: sub;
21
21
  }
22
22
 
@@ -5,6 +5,6 @@ module Dokno
5
5
  include UserConcern
6
6
  include PaginationConcern
7
7
 
8
- add_flash_types :green, :yellow, :red
8
+ add_flash_types :green, :yellow, :red, :gray
9
9
  end
10
10
  end
@@ -14,7 +14,13 @@ module Dokno
14
14
  @category = Category.find_by(code: params[:cat_code].to_s.strip) if params[:cat_code].present?
15
15
  @category = @article.categories.first if @category.blank?
16
16
 
17
- flash.now[:yellow] = 'This article is no longer active' unless @article.active
17
+ if !@article.active
18
+ flash.now[:yellow] = 'This article is no longer active'
19
+ elsif @article.up_for_review?
20
+ flash_msg = @article.review_due_days_string
21
+ flash_msg += " - <a href='#{edit_article_path(@article.slug)}' class='font-bold'>review it now</a>" if can_edit?
22
+ flash.now[:gray] = flash_msg
23
+ end
18
24
  end
19
25
 
20
26
  def new
@@ -33,7 +39,7 @@ module Dokno
33
39
  set_editor_username
34
40
 
35
41
  if @article.save
36
- flash[:green] = 'Article was created'
42
+ flash[:green] = 'Article was created'
37
43
  @article.categories = Category.where(code: params[:category_code]) if params[:category_code].present?
38
44
  redirect_to article_path @article.slug
39
45
  else
@@ -50,12 +56,14 @@ module Dokno
50
56
  set_editor_username
51
57
 
52
58
  if @article.update(article_params)
53
- flash[:green] = 'Article was updated'
59
+ flash[:green] = 'Article was updated'
54
60
  @article.categories = Category.where(code: params[:category_code])
55
61
  redirect_to article_path @article.slug
56
62
  else
57
- flash.now[:red] = 'Article could not be updated'
58
- @category_codes = params[:category_code]
63
+ flash.now[:red] = 'Article could not be updated'
64
+ @category_codes = params[:category_code]
65
+ @reset_review_date = params[:reset_review_date]
66
+ @review_notes = params[:review_notes]
59
67
  render :edit
60
68
  end
61
69
  end
@@ -88,7 +96,7 @@ module Dokno
88
96
  private
89
97
 
90
98
  def article_params
91
- params.permit(:slug, :title, :summary, :markdown)
99
+ params.permit(:slug, :title, :summary, :markdown, :reset_review_date, :review_notes, :starred)
92
100
  end
93
101
 
94
102
  def fetch_article
@@ -10,7 +10,9 @@ module Dokno
10
10
  @order = params[:order]&.strip
11
11
  @order = 'updated' unless %w(updated newest views alpha).include?(@order)
12
12
 
13
- articles = if @search_term.present?
13
+ articles = if request.path.include? up_for_review_path
14
+ Article.up_for_review(order: @order&.to_sym)
15
+ elsif @search_term.present?
14
16
  Article.search(term: @search_term, category_id: @category&.id, order: @order&.to_sym)
15
17
  elsif @category.present?
16
18
  @category.articles_in_branch(order: @order&.to_sym)
@@ -22,7 +24,7 @@ module Dokno
22
24
  end
23
25
 
24
26
  def new
25
- @category = Category.new
27
+ @category = Category.new
26
28
  @parent_category_code = params[:parent_category_code]
27
29
  end
28
30
 
@@ -38,7 +40,7 @@ module Dokno
38
40
  flash[:green] = 'Category was created'
39
41
  redirect_to article_index_path(@category.code)
40
42
  else
41
- flash.now[:red] = 'Category could not be created'
43
+ flash.now[:red] = 'Category could not be created'
42
44
  @parent_category_code = params[:parent_category_code]
43
45
  render :new
44
46
  end
@@ -51,7 +53,7 @@ module Dokno
51
53
  flash[:green] = 'Category was updated'
52
54
  redirect_to article_index_path(@category.code)
53
55
  else
54
- flash.now[:red] = 'Category could not be updated'
56
+ flash.now[:red] = 'Category could not be updated'
55
57
  @parent_category_code = params[:parent_category_code]
56
58
  render :edit
57
59
  end
@@ -15,19 +15,28 @@ module Dokno
15
15
  validates :title, length: { in: 5..255 }
16
16
  validate :unique_slug_check
17
17
 
18
+ attr_accessor :editor_username, :reset_review_date, :review_notes
19
+
20
+ before_save :set_review_date, if: :should_set_review_date?
18
21
  before_save :log_changes
19
22
  before_save :track_slug
20
23
 
21
24
  scope :active, -> { where(active: true) }
22
- scope :alpha_order, -> { order(active: :desc, title: :asc) }
23
- scope :view_order, -> { order(active: :desc, views: :desc, title: :asc) }
24
- scope :newest_order, -> { order(active: :desc, created_at: :desc, title: :asc) }
25
- scope :updated_order, -> { order(active: :desc, updated_at: :desc, title: :asc) }
26
-
27
- attr_accessor :editor_username
25
+ scope :alpha_order, -> { order(active: :desc, starred: :desc, title: :asc) }
26
+ scope :views_order, -> { order(active: :desc, starred: :desc, views: :desc, title: :asc) }
27
+ scope :newest_order, -> { order(active: :desc, starred: :desc, created_at: :desc, title: :asc) }
28
+ scope :updated_order, -> { order(active: :desc, starred: :desc, updated_at: :desc, title: :asc) }
28
29
 
29
30
  MARKDOWN_PARSER = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
30
31
 
32
+ def review_due_at
33
+ super || (Date.today + 30.years)
34
+ end
35
+
36
+ def markdown
37
+ super || ''
38
+ end
39
+
31
40
  def reading_time
32
41
  minutes_decimal = (("#{summary} #{markdown}".squish.scan(/[\w-]+/).size) / 200.0)
33
42
  approx_minutes = minutes_decimal.ceil
@@ -36,8 +45,22 @@ module Dokno
36
45
  "~ #{approx_minutes} minutes"
37
46
  end
38
47
 
39
- def markdown
40
- super || ''
48
+ def review_due_days
49
+ (review_due_at.to_date - Date.today).to_i
50
+ end
51
+
52
+ def up_for_review?
53
+ active && review_due_days <= Dokno.config.article_review_prompt_days
54
+ end
55
+
56
+ def review_due_days_string
57
+ if review_due_days.positive?
58
+ "This article is up for an accuracy / relevance review in #{review_due_days} #{'day'.pluralize(review_due_days)}"
59
+ elsif review_due_days.negative?
60
+ "This article was up for an accuracy / relevance review #{review_due_days.abs} #{'day'.pluralize(review_due_days)} ago"
61
+ else
62
+ "This article is up for an accuracy / relevance review today"
63
+ end
41
64
  end
42
65
 
43
66
  def markdown_parsed
@@ -112,34 +135,24 @@ module Dokno
112
135
  .to_sentence
113
136
  end
114
137
 
115
- # All uncategorized Articles
116
- def self.uncategorized(order: :updated)
138
+ # All articles up for review
139
+ def self.up_for_review(order: :updated)
117
140
  records = Article
118
141
  .includes(:categories_dokno_articles, :categories)
119
- .left_joins(:categories)
120
- .where(active: true, dokno_categories: { id: nil })
142
+ .where(active: true)
143
+ .where('review_due_at <= ?', Date.today + Dokno.config.article_review_prompt_days)
121
144
 
122
- records = records.updated_order if order == :updated
123
- records = records.newest_order if order == :newest
124
- records = records.view_order if order == :views
125
- records = records.alpha_order if order == :alpha
126
-
127
- records
145
+ apply_sort(records, order: order)
128
146
  end
129
147
 
130
- def self.parse_markdown(content)
131
- ActionController::Base.helpers.sanitize(
132
- MARKDOWN_PARSER.render(content),
133
- tags: Dokno.config.tag_whitelist,
134
- attributes: Dokno.config.attr_whitelist
135
- )
136
- end
137
-
138
- def self.template
139
- template_file = File.join(Rails.root, 'config', 'dokno_template.md')
140
- return unless File.exist?(template_file)
148
+ # All uncategorized Articles
149
+ def self.uncategorized(order: :updated)
150
+ records = Article
151
+ .includes(:categories_dokno_articles, :categories)
152
+ .left_joins(:categories)
153
+ .where(dokno_categories: { id: nil })
141
154
 
142
- File.read(template_file).to_s
155
+ apply_sort(records, order: order)
143
156
  end
144
157
 
145
158
  def self.search(term:, category_id: nil, order: :updated)
@@ -153,10 +166,7 @@ module Dokno
153
166
  .includes(:categories_dokno_articles)
154
167
  .includes(:categories)
155
168
 
156
- records = records.updated_order if order == :updated
157
- records = records.newest_order if order == :newest
158
- records = records.view_order if order == :views
159
- records = records.alpha_order if order == :alpha
169
+ records = apply_sort(records, order: order)
160
170
 
161
171
  return records unless category_id.present?
162
172
 
@@ -170,6 +180,28 @@ module Dokno
170
180
  )
171
181
  end
172
182
 
183
+ def self.parse_markdown(content)
184
+ ActionController::Base.helpers.sanitize(
185
+ MARKDOWN_PARSER.render(content),
186
+ tags: Dokno.config.tag_whitelist,
187
+ attributes: Dokno.config.attr_whitelist
188
+ )
189
+ end
190
+
191
+ def self.template
192
+ template_file = File.join(Rails.root, 'config', 'dokno_template.md')
193
+ return unless File.exist?(template_file)
194
+
195
+ File.read(template_file).to_s
196
+ end
197
+
198
+ def self.apply_sort(records, order: :updated)
199
+ order_scope = "#{order}_order"
200
+ return records unless records.respond_to? order_scope
201
+
202
+ records.send(order_scope.to_sym)
203
+ end
204
+
173
205
  private
174
206
 
175
207
  # Ensure there isn't another Article with the same slug
@@ -189,7 +221,7 @@ module Dokno
189
221
  end
190
222
 
191
223
  def log_changes
192
- return if changes.blank?
224
+ return if changes.blank? && !reset_review_date
193
225
 
194
226
  meta_changes = changes.with_indifferent_access.slice(:slug, :title, :active)
195
227
  content_changes = changes.with_indifferent_access.slice(:summary, :markdown)
@@ -207,10 +239,26 @@ module Dokno
207
239
  content[:after] += values.last.to_s + ' '
208
240
  end
209
241
 
210
- return unless meta.present?
242
+ if meta.present?
243
+ diff = Diffy::SplitDiff.new(content[:before].squish, content[:after].squish, format: :html)
244
+ logs << Log.new(username: editor_username, meta: meta.to_sentence, diff_left: diff.left, diff_right: diff.right)
245
+ end
246
+
247
+ # Reviewed for accuracy / relevance?
248
+ return unless reset_review_date
249
+
250
+ review_log = "Reviewed for accuracy / relevance. Next review date reset to #{review_due_at.to_date}."
251
+ review_log += " Review notes: #{review_notes.squish}" if review_notes.present?
252
+ logs << Log.new(username: editor_username, meta: review_log)
253
+ end
254
+
255
+ def should_set_review_date?
256
+ # User requested a reset or it's a new article w/out a review due date
257
+ reset_review_date || (!persisted? && review_due_at.blank?)
258
+ end
211
259
 
212
- diff = Diffy::SplitDiff.new(content[:before].squish, content[:after].squish, format: :html)
213
- logs << Log.new(username: editor_username, meta: meta.to_sentence, diff_left: diff.left, diff_right: diff.right)
260
+ def set_review_date
261
+ self.review_due_at = Date.today + Dokno.config.article_review_period
214
262
  end
215
263
  end
216
264
  end
@@ -45,12 +45,7 @@ module Dokno
45
45
  .joins(:categories)
46
46
  .where(dokno_categories: { id: self.class.branch(parent_category_id: id).pluck(:id) })
47
47
 
48
- records = records.updated_order if order == :updated
49
- records = records.newest_order if order == :newest
50
- records = records.view_order if order == :views
51
- records = records.alpha_order if order == :alpha
52
-
53
- records
48
+ Article.apply_sort(records, order: order)
54
49
  end
55
50
 
56
51
  def branch
@@ -59,7 +54,7 @@ module Dokno
59
54
 
60
55
  # Used to invalidate the fragment cache of the hierarchical category select options
61
56
  def self.cache_key
62
- updated_timestamps = [maximum(:updated_at), Article.maximum(:updated_at)].compact.max
57
+ [maximum(:updated_at), Article.maximum(:updated_at)].compact.max
63
58
  end
64
59
 
65
60
  # The given Category and all child Categories. Useful for filtering associated articles.
@@ -3,8 +3,31 @@
3
3
  <section class="mb-10">
4
4
 
5
5
  <div class="text-2xl mb-5 text-gray-500"><%= article.persisted? ? 'Edit' : 'New' %> Article</div>
6
- <h1 class="text-4xl mb-5 leading-tight font-light"><%= article.title %></h1>
7
- <hr class="mb-5" />
6
+ <h1 class="text-4xl leading-tight font-light"><%= article.title %></h1>
7
+ <hr class="mt-8 mb-10" />
8
+
9
+ <% if article.up_for_review? %>
10
+ <div class="bg-gray-800 text-gray-100 rounded mb-10">
11
+ <h2 class="bg-gray-900 p-10 rounded-t text-2xl leading-tight font-light">Review for Accuracy / Relevance</h2>
12
+
13
+ <div class="flex p-10">
14
+ <div class="w-1/2 pr-5">
15
+ <div>
16
+ <span class="text-lg font-semibold"><label for="reset_review_date">Reset the next review date?</label></span>
17
+ <input onchange="setReviewForm();" type="checkbox" name="reset_review_date" id="reset_review_date" value="true" class="ml-2 text-lg" <%= "checked='checked'" if @reset_review_date %> />
18
+ </div>
19
+ <div class="text-gray-400">
20
+ <%= article.review_due_days_string %>.
21
+ Check the box above to mark this article as reviewed and reset the next review date to <span class="px-2 py-1 bg-gray-900 rounded text-white font-semibold"><%= "#{Date.today + Dokno.config.article_review_period}" %></span>
22
+ </div>
23
+ </div>
24
+ <div class="w-1/2">
25
+ <div class="text-lg font-semibold"><label for="review_notes">Reviewer Notes</label></div>
26
+ <textarea placeholder="Any review notes here will be added to the change history for this article" name="review_notes" id="review_notes" class="rounded text-xl shadow-inner bg-gray-100 p-3 mt-2 w-full text-gray-900" rows="2"><%= @review_notes %></textarea>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ <% end %>
8
31
 
9
32
  <div class="mb-5">
10
33
  <div class="text-lg font-semibold"><label for="slug">Slug</label></div>
@@ -27,7 +50,7 @@
27
50
  </div>
28
51
  <% end %>
29
52
 
30
- <hr class="mb-5" />
53
+ <hr class="my-10" />
31
54
 
32
55
  <div class="mb-5">
33
56
  <div class="text-lg font-semibold"><label for="title">Title</label></div>
@@ -52,11 +75,27 @@
52
75
  <div class="text-gray-500 mt-2">Detailed documentation of the described topic. Basic HTML &amp; <a target="_blank" href="https://commonmark.org/help/" title="Markdown formatting examples">markdown</a> OK.</div>
53
76
  </div>
54
77
 
55
- <div class="mt-10 text-right">
78
+ <div class="mb-5">
79
+ <div>
80
+ <span class="text-lg font-semibold"><label for="starred">Starred article?</label></span>
81
+ <input type="hidden" name="starred" value="false" />
82
+ <input type="checkbox" name="starred" id="starred" value="true" class="ml-2 text-lg" <%= "checked='checked'" if article.starred %> />
83
+ </div>
84
+ <div class="text-gray-400">
85
+ Starred articles are always listed first when browsing.
86
+ </div>
87
+ </div>
88
+
89
+ <hr class="my-10" />
90
+
91
+ <div class="text-right">
56
92
  <span class="text-lg mr-5"><a class="no-underline" href="<%= article.persisted? ? article_path(article.slug) : root_path %>">Cancel</a></span>
57
93
  <button type="submit" class="bg-gray-700 text-gray-300 hover:bg-gray-900 hover:text-white rounded py-2 px-3 font-bold"><i data-feather="check" class="inline h-5"></i> SAVE ARTICLE</button>
58
94
  </div>
59
95
 
60
96
  </section>
61
97
 
62
- <script> elem('input#slug').focus(); </script>
98
+ <script>
99
+ elem('input#slug').focus();
100
+ setReviewForm();
101
+ </script>
@@ -13,7 +13,10 @@
13
13
  <section>
14
14
  <div class="flex">
15
15
  <div id="dokno-article-sidebar" class="w-2/5 pr-10">
16
- <h1 class="dokno-article-content-highlight text-4xl mb-8 leading-tight font-light"><%= @article.title %></h1>
16
+ <h1 class="dokno-article-content-highlight text-4xl mb-8 leading-tight font-light">
17
+ <%= @article.title %>
18
+ <% if @article.starred %><i title="Starred article" data-feather="star" class="inline align-middle"></i><% end %>
19
+ </h1>
17
20
 
18
21
  <div>
19
22
  <div class="mb-8 no-print">
@@ -23,7 +26,7 @@
23
26
  </div>
24
27
 
25
28
  <div class="flex">
26
- <div class="no-print w-8"><i data-feather="clock" class="inline-block h-5"></i></div>
29
+ <div class="no-print w-8"><i data-feather="clock" class="inline-block h-5 mr-2"></i></div>
27
30
  <div class="w-full">
28
31
  Last updated:<br />
29
32
  <%= time_ago_in_words @article.updated_at %> ago
@@ -34,7 +37,7 @@
34
37
  </div>
35
38
 
36
39
  <div class="mt-5 flex no-print">
37
- <div class="no-print w-8"><i data-feather="eye" class="inline-block h-5"></i></div>
40
+ <div class="no-print w-8"><i data-feather="eye" class="inline-block h-5 mr-2"></i></div>
38
41
  <div class="w-full">
39
42
  Views:<br />
40
43
  <%= number_with_delimiter(@article.views, delimiter: ',') %>
@@ -43,7 +46,7 @@
43
46
 
44
47
  <% if @article.contributors.present? %>
45
48
  <div class="mt-5 flex">
46
- <div class="no-print w-8"><i data-feather="user-check" class="inline-block h-5"></i></div>
49
+ <div class="no-print w-8"><i data-feather="user-check" class="inline-block h-5 mr-2"></i></div>
47
50
  <div class="w-full">
48
51
  Contributors:<br />
49
52
  <%= @article.contributors %>
@@ -53,7 +56,7 @@
53
56
 
54
57
  <% if @article.reading_time.present? %>
55
58
  <div class="mt-5 flex no-print">
56
- <div class="no-print w-8"><i data-feather="watch" class="inline-block h-5"></i></div>
59
+ <div class="no-print w-8"><i data-feather="watch" class="inline-block h-5 mr-2"></i></div>
57
60
  <div class="w-full">
58
61
  Reading time:<br />
59
62
  <%= @article.reading_time %>
@@ -62,7 +65,7 @@
62
65
  <% end %>
63
66
 
64
67
  <div class="mt-5 flex">
65
- <div class="no-print w-8"><i data-feather="link" class="inline-block h-5"></i></div>
68
+ <div class="no-print w-8"><i data-feather="link" class="inline-block h-5 mr-2"></i></div>
66
69
  <div class="w-full">
67
70
  Permalink:<br />
68
71
  <a class="inline-block mt-1 -ml-2 px-2 py-1 bg-blue-100 rounded" title="Copy to clipboard" href="javascript:;" onclick="copyToClipboard('<%= @article.permalink(request.base_url) %>');"><%= @article.permalink(request.base_url) %></a>
@@ -71,7 +74,7 @@
71
74
 
72
75
  <% if can_edit? %>
73
76
  <div class="mt-5 flex no-print">
74
- <div class="no-print w-8"><i data-feather="crosshair" class="inline-block h-5"></i></div>
77
+ <div class="no-print w-8"><i data-feather="crosshair" class="inline-block h-5 mr-2"></i></div>
75
78
  <div class="w-full">
76
79
  Unique slug:<br />
77
80
  <a class="inline-block mt-1 -ml-2 px-2 py-1 bg-blue-100 rounded" title="Copy to clipboard" href="javascript:;" onclick="copyToClipboard('<%= j @article.slug %>');"><%= @article.slug %></a>
@@ -90,7 +93,7 @@
90
93
 
91
94
  <% if (category_name_list = @article.category_name_list(context_category_id: @category&.id)).present? %>
92
95
  <div class="mt-5 flex no-print">
93
- <div class="no-print w-8"><i data-feather="folder" class="inline-block h-5"></i></div>
96
+ <div class="no-print w-8"><i data-feather="folder" class="inline-block h-5 mr-2"></i></div>
94
97
  <div class="w-full">
95
98
  <%= category_name_list.sub(':', ':<br />').html_safe %>
96
99
  </div>
@@ -120,14 +123,14 @@
120
123
  <div class="no-print">
121
124
  <hr class="mt-5 mb-10" />
122
125
  <div class="text-right">
123
- <button title="Edit article" class="bg-blue-900 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-sm" onclick="location.href='<%= edit_article_path(@article.slug) %>';"><i data-feather="edit" class="inline h-5"></i> EDIT</button>
126
+ <button title="Edit article" class="bg-blue-900 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-sm" onclick="location.href='<%= edit_article_path(@article.slug) %>';"><i data-feather="edit-2" class="inline h-5"></i> EDIT</button>
124
127
 
125
128
  <% if @article.active %>
126
- <button title="Deactivate article" id="article-deactivate-button" class="bg-yellow-700 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-base" onclick="deactivateArticle('<%= @article.slug %>');"><i data-feather="sunset" class="inline h-5"></i> DEACTIVATE</button>
129
+ <button title="Deactivate article" id="article-deactivate-button" class="bg-yellow-700 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-base" onclick="deactivateArticle('<%= @article.slug %>');"><i data-feather="file-minus" class="inline h-5"></i> DEACTIVATE</button>
127
130
  <% else %>
128
- <button title="Re-activate article" id="article-activate-button" class="bg-green-700 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-base" onclick="activateArticle('<%= @article.slug %>');"><i data-feather="sunrise" class="inline h-5"></i> RE-ACTIVATE</button>
131
+ <button title="Re-activate article" id="article-activate-button" class="bg-green-700 text-gray-300 hover:text-white rounded mr-5 py-2 px-3 font-bold text-base" onclick="activateArticle('<%= @article.slug %>');"><i data-feather="file-plus" class="inline h-5"></i> RE-ACTIVATE</button>
129
132
  <% end %>
130
- <button title="Permanently delete article" class="bg-red-700 text-gray-300 hover:text-white rounded py-2 px-3 font-bold text-base" onclick="deleteArticle('<%= @article.id %>');"><i data-feather="trash" class="inline h-5"></i> DELETE</button>
133
+ <button title="Permanently delete article" class="bg-red-700 text-gray-300 hover:text-white rounded py-2 px-3 font-bold text-base" onclick="deleteArticle('<%= @article.id %>');"><i data-feather="slash" class="inline h-5"></i> DELETE</button>
131
134
  </div>
132
135
  </div>
133
136
  <% end %>
@@ -4,7 +4,7 @@
4
4
  </div>
5
5
  <% end %>
6
6
 
7
- <% if Dokno::Category.exists? || Dokno::Article.exists? %>
7
+ <% if !current_page?(up_for_review_path) && (Dokno::Category.exists? || Dokno::Article.exists?) %>
8
8
  <%= render 'partials/category_header' %>
9
9
  <% end %>
10
10
 
@@ -44,7 +44,7 @@
44
44
  <section class="border-t border-gray-300 py-10 text-xl flex">
45
45
  <div class="w-1/3 pr-10">
46
46
  <div class="flex">
47
- <div class="no-print w-10 text-gray-300"><i data-feather="chevron-right" class="inline-block"></i></div>
47
+ <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>
48
48
  <div class="w-full">
49
49
  <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>
50
50
  </div>
@@ -53,7 +53,7 @@
53
53
  <div class="dokno-article-summary w-2/3 <% unless article.active %>text-gray-500 italic<% end %>">
54
54
  <% unless article.active %>
55
55
  <div class="bg-yellow-700 p-4 mb-5 rounded text-lg border-t-4 border-yellow-900 text-white font-base not-italic">
56
- <i data-feather="alert-circle" class="inline-block"></i> This article is no longer active
56
+ <i data-feather="info" class="inline-block"></i> This article is no longer active
57
57
  </div>
58
58
  <% end %>
59
59
 
@@ -66,14 +66,21 @@
66
66
  <% unless @order == 'alpha' %>
67
67
  <div class="text-base">
68
68
  <% if @order == 'views' %>
69
- <div class="text-gray-500">Viewed <%= number_with_delimiter(article.views, delimiter: ',') %> <%= 'time'.pluralize(article.views) %></div>
69
+ <div class="text-gray-500">This article was viewed <%= number_with_delimiter(article.views, delimiter: ',') %> <%= 'time'.pluralize(article.views) %></div>
70
70
  <% elsif @order == 'updated' %>
71
- <div class="text-gray-500">Last updated <%= time_ago_in_words article.updated_at %> ago</div>
71
+ <div class="text-gray-500">This article was last updated <%= time_ago_in_words article.updated_at %> ago</div>
72
72
  <% elsif @order == 'newest' %>
73
- <div class="text-gray-500">Added <%= time_ago_in_words article.created_at %> ago</div>
73
+ <div class="text-gray-500">This article was added <%= time_ago_in_words article.created_at %> ago</div>
74
74
  <% end %>
75
75
  </div>
76
76
  <% end %>
77
+
78
+ <% if article.up_for_review? %>
79
+ <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">
80
+ <i data-feather="bell" class="inline-block mr-1"></i> <%= article.review_due_days_string %>
81
+ </div>
82
+ <% end %>
83
+
77
84
  </div>
78
85
  </section>
79
86
  <% 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
 
@@ -24,12 +26,12 @@
24
26
  <div class="w-2/3 text-right">
25
27
  <% if can_edit? %>
26
28
  <% 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>
29
+ <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>
30
+ <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
31
  <% end %>
30
32
 
31
33
  <% 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>
34
+ <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
35
  <% end %>
34
36
  <% end %>
35
37
 
@@ -40,15 +42,15 @@
40
42
  </div>
41
43
  </nav>
42
44
 
43
- <% flash.each do |color, msg| %>
44
- <div class="bg-<%= color %>-700 text-lg text-white font-base py-10 px-16">
45
- <i data-feather="<%= (color == 'green' ? 'check-circle' : (color == 'yellow' ? 'alert-circle' : 'x-circle')) %>" class="inline mr-2"></i>
46
- <%= msg %>
45
+ <% flash.each do |type, msg| %>
46
+ <div class="bg-<%= type %>-800 text-lg text-white font-base py-10 px-16 text-center">
47
+ <i data-feather="<%= (type == 'green' ? 'smile' : (type == 'yellow' ? 'info' : (type == 'gray' ? 'bell' : 'frown'))) %>" class="inline mr-1"></i>
48
+ <%= sanitize(msg, tags: %w[a], attributes: %w[href class]) %>
47
49
  </div>
48
50
  <% end %>
49
51
 
50
52
  <main class="py-10 px-16">
51
- <% if @article.blank? && (@category.blank? || @search_term.present?) %>
53
+ <% if !current_page?(up_for_review_path) && @article.blank? && (@category.blank? || @search_term.present?) %>
52
54
  <div class="text-center m-auto mb-10 w-full max-w-screen-xl">
53
55
  <% if @search_term.present? %>
54
56
  <div class="text-gray-600 text-2xl uppercase">
@@ -81,10 +83,24 @@
81
83
  </div>
82
84
  <% end %>
83
85
 
86
+ <% if can_edit? && !current_page?(up_for_review_path) && action_name == 'index' && (up_for_review_count = Dokno::Article.up_for_review.count).positive? %>
87
+ <div id="dokno-articles-up-for-review-container">
88
+ <div class="py-10 px-16 bg-gray-900">
89
+ <div class="w-full max-w-screen-xl m-auto">
90
+ <div class="text-xl text-white cursor-pointer" onclick="location.href='<%= up_for_review_path %>';">
91
+ <i data-feather="bell" class="inline mr-1"></i>
92
+ There <%= "#{up_for_review_count == 1 ? 'is' : 'are'} #{up_for_review_count}" %> <%= 'article'.pluralize(up_for_review_count) %> up for accuracy / relevance review
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ <% end %>
98
+
84
99
  <div class="py-10 px-16 text-gray-400 bg-blue-900">
85
100
  <div class="w-full max-w-screen-xl m-auto flex">
86
101
  <div class="w-1/2">
87
- <a target="_blank" href="https://github.com/cpayne624/dokno" title="Knowledgebase">dokno</a>
102
+ <i data-feather="github" class="inline mr-1"></i>
103
+ <a target="_blank" href="https://github.com/cpayne624/dokno" title="Dokno on GitHub">dokno</a>
88
104
  </div>
89
105
  <div class="w-1/2 text-right">
90
106
  <% if user.present? %>
@@ -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>
@@ -3,6 +3,7 @@ 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
8
  post 'article_preview', to: 'articles#preview', as: :preview
8
9
  post 'article_status', to: 'articles#status', as: :status
@@ -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
@@ -4,10 +4,6 @@ module Dokno
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace Dokno
6
6
 
7
- initializer 'Dokno precompile', group: :all do |app|
8
- app.config.assets.precompile << "dokno_manifest.js"
9
- end
10
-
11
7
  config.generators do |g|
12
8
  g.test_framework :rspec
13
9
  end
@@ -18,6 +14,10 @@ module Dokno
18
14
  end
19
15
  end
20
16
 
17
+ initializer 'Dokno precompile', group: :all do |app|
18
+ app.config.assets.precompile << "dokno_manifest.js"
19
+ end
20
+
21
21
  initializer 'local_helper.action_controller' do
22
22
  ActiveSupport.on_load :action_controller do
23
23
  helper Dokno::ApplicationHelper
@@ -1,3 +1,3 @@
1
1
  module Dokno
2
- VERSION = '1.2.1'
2
+ VERSION = '1.3.0'
3
3
  end
@@ -1,14 +1,27 @@
1
1
  Dokno.configure do |config|
2
- config.tag_whitelist = Dokno::Config::TAG_WHITELIST
3
- config.attr_whitelist = Dokno::Config::ATTR_WHITELIST
2
+ # To control the permitted HTML tags and attributes within articles,
3
+ # uncomment and change the defaults.
4
+ # (Enumerable) tag_whitelist
5
+ # (Enumerable) attr_whitelist
6
+ # config.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]
7
+ # config.attr_whitelist = %w[src alt title href target]
4
8
 
5
9
  # To restrict Dokno data modification and include indentifying information
6
10
  # in change log entries, provide the appropriate user values for your app below.
7
11
  # (String) app_user_object
8
12
  # (Symbol) app_user_auth_method
9
13
  # (Symbol) app_user_name_method
14
+ # config.app_user_object = 'current_user'
15
+ # config.app_user_auth_method = :admin?
16
+ # config.app_user_name_method = :name
10
17
 
11
- # config.app_user_object = 'current_user'
12
- # config.app_user_auth_method = :admin?
13
- # config.app_user_name_method = :name
18
+ # To control the amount of time before a created/updated article is flagged
19
+ # for accuracy/relevance review, uncomment and change the default.
20
+ # (ActiveSupport::Duration) article_review_period
21
+ # config.article_review_period = 1.year
22
+
23
+ # To control the number of days prior to an article being up for review
24
+ # that users should be prompted to re-review, uncomment and change the default.
25
+ # (Integer) article_review_prompt_days
26
+ # config.article_review_prompt_days = 30
14
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dokno
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Courtney Payne
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-08 00:00:00.000000000 Z
11
+ date: 2020-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diffy
@@ -284,6 +284,8 @@ files:
284
284
  - app/views/partials/_pagination.html.erb
285
285
  - config/routes.rb
286
286
  - db/migrate/20201203190330_baseline.rb
287
+ - db/migrate/20201211192306_add_review_due_at_to_articles.rb
288
+ - db/migrate/20201213165700_add_starred_to_article.rb
287
289
  - lib/dokno.rb
288
290
  - lib/dokno/config/config.rb
289
291
  - lib/dokno/engine.rb
@@ -298,7 +300,7 @@ licenses:
298
300
  metadata:
299
301
  bug_tracker_uri: https://github.com/cpayne624/dokno/issues
300
302
  changelog_uri: https://github.com/cpayne624/dokno/blob/master/CHANGELOG.md
301
- post_install_message:
303
+ post_install_message:
302
304
  rdoc_options: []
303
305
  require_paths:
304
306
  - lib
@@ -313,8 +315,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
315
  - !ruby/object:Gem::Version
314
316
  version: '0'
315
317
  requirements: []
316
- rubygems_version: 3.1.4
317
- signing_key:
318
+ rubygems_version: 3.2.0.rc.1
319
+ signing_key:
318
320
  specification_version: 4
319
321
  summary: Dokno (dough-no) is a lightweight mountable Rails Engine for storing and
320
322
  managing your app's domain knowledge.