dokno 1.1.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/app/assets/javascripts/dokno.js +67 -31
  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 +24 -45
  14. data/app/views/dokno/_article_formatting.html.erb +17 -18
  15. data/app/views/dokno/_article_panel.html.erb +10 -19
  16. data/app/views/dokno/_panel_formatting.html.erb +47 -57
  17. data/app/views/dokno/articles/_article_form.html.erb +49 -10
  18. data/app/views/dokno/articles/show.html.erb +48 -45
  19. data/app/views/dokno/categories/_category_form.html.erb +18 -7
  20. data/app/views/dokno/categories/index.html.erb +59 -40
  21. data/app/views/layouts/dokno/application.html.erb +36 -35
  22. data/app/views/partials/_category_header.html.erb +30 -0
  23. data/app/views/partials/_form_errors.html.erb +0 -1
  24. data/app/views/partials/_logs.html.erb +7 -5
  25. data/app/views/partials/_pagination.html.erb +20 -18
  26. data/config/routes.rb +1 -1
  27. data/db/migrate/20201203190330_baseline.rb +4 -4
  28. data/db/migrate/20201211192306_add_review_due_at_to_articles.rb +6 -0
  29. data/db/migrate/20201213165700_add_starred_to_article.rb +5 -0
  30. data/lib/dokno/config/config.rb +53 -40
  31. data/lib/dokno/engine.rb +5 -5
  32. data/lib/dokno/version.rb +1 -1
  33. data/lib/generators/dokno/templates/config/initializers/dokno.rb +18 -5
  34. metadata +82 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df8bbd2d17e134f0298f31fc97d049b245c4830e9d44655a8c4659507c6ebe6f
4
- data.tar.gz: bdeb9513295036198a8f30a476a1ea8a4c34d35330c9e6e75e2364aadddf0b3b
3
+ metadata.gz: f136bc4f785f0f392e5e76d19b55d74ae59f59f96f144049cb9e288aaaaeced5
4
+ data.tar.gz: 117835447f5ef02db4c18de8453f7053a977cec6349528a6b3d15250da42ae0a
5
5
  SHA512:
6
- metadata.gz: 97c1353f3c85e9a6e40de60a0947101978bb101b0dc85be48945ee7e54bb13596fd0f1afbd011113f538c5dfe0ed8784c09c87643b5813e5021292c49244273b
7
- data.tar.gz: 1bfda031b688b21b65fc8b43fd550e006fec2d5b5f1fd50d4f536db9724ef4853ed47f49e8b2e3ed3329f9ecd6092eb2d678dc6562fa1c07d092da0412ff1e12
6
+ metadata.gz: a49495dd1c51f2eac0508d8df2a7bc34f97a8f7cfa2699f36a3cebfbcc3b5012c7ccf80f1bcc730815a644b08ac4021e02fa0f5fd2b39bac1c86d8c0cbbed13b
7
+ data.tar.gz: ebe01549788e0f4ab21eb36b1b11a0b7d21518dc4626cba64543e6c3ca66a6de8ea7bc311df6490489df22f51f239b0cb544af79bb2ce7af146a6938be829b8b
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
 
@@ -119,6 +117,8 @@ $ bundle exec rspec
119
117
  ```
120
118
 
121
119
  ## Hat Tips
120
+ - [tailwindcss](https://tailwindcss.com/)
121
+ - CSS framework
122
122
  - [diffy](https://github.com/samg/diffy)
123
123
  - Text diffing Ruby gem
124
124
  - [Feather Icons](https://github.com/feathericons/feather)
@@ -1,5 +1,23 @@
1
+ const dokno__search_hotkey_listener = function(e) {
2
+ if (e.key === '/') { handleSearchHotKey(); }
3
+ }
4
+
5
+ function handleSearchHotKey() {
6
+ const search_input = elem('input#search_term');
7
+ search_input.focus();
8
+ search_input.select();
9
+ }
10
+
11
+ function enableSearchHotkey() {
12
+ document.addEventListener('keyup', dokno__search_hotkey_listener, false);
13
+ }
14
+
15
+ function disableSearchHotkey() {
16
+ document.removeEventListener('keyup', dokno__search_hotkey_listener, false);
17
+ }
18
+
1
19
  function copyToClipboard(text) {
2
- window.prompt('Copy to clipboard: CTRL+C, Enter', text);
20
+ window.prompt('Copy to clipboard: CTRL + C, Enter', text);
3
21
  }
4
22
 
5
23
  function elem(selector) {
@@ -10,6 +28,32 @@ function elems(selector) {
10
28
  return document.getElementsByClassName(selector);
11
29
  }
12
30
 
31
+ function selectOption(id, value) {
32
+ var sel = elem('#' + id);
33
+ var opts = sel.options;
34
+
35
+ for (var opt, j = 0; opt = opts[j]; j++) {
36
+ if (opt.value == value) {
37
+ sel.selectedIndex = j;
38
+ break;
39
+ }
40
+ }
41
+ }
42
+
43
+ function applyCategoryCriteria(category_code, term, order) {
44
+ goToPage(dokno__base_path + category_code + '?search_term=' + term + '&order=' + order);
45
+ }
46
+
47
+ function goToPage(url) {
48
+ var param_join = url.indexOf('?') >= 0 ? '&' : '?';
49
+ location.href = url + param_join + '_=' + Math.round(new Date().getTime());
50
+ }
51
+
52
+ function reloadPage() {
53
+ window.onbeforeunload = function () { window.scrollTo(0, 0); }
54
+ location.reload();
55
+ }
56
+
13
57
  function sendRequest(url, data, callback, method) {
14
58
  const request = new XMLHttpRequest();
15
59
  request.open(method, url, true);
@@ -30,24 +74,9 @@ function sendRequest(url, data, callback, method) {
30
74
  request.send(JSON.stringify(data));
31
75
  }
32
76
 
33
- function deactiveArticle(slug) {
34
- const callback = function(_data) {
35
- elem('button#article-deactivate-button').classList.add('hidden');
36
- elem('div#article-deprecated-alert').classList.remove('hidden');
37
- elem('button#article-activate-button').classList.remove('hidden');
38
- reloadLogs();
39
- }
40
- sendRequest(dokno__base_path + 'article_status', { slug: slug, active: false }, callback, 'POST');
41
- }
42
-
43
- function activeArticle(slug) {
44
- const callback = function(_data) {
45
- elem('button#article-activate-button').classList.add('hidden');
46
- elem('div#article-deprecated-alert').classList.add('hidden');
47
- elem('button#article-deactivate-button').classList.remove('hidden');
48
- reloadLogs();
49
- }
50
- sendRequest(dokno__base_path + 'article_status', { slug: slug, active: true }, callback, 'POST');
77
+ function setArticleStatus(slug, active) {
78
+ const callback = function(_data) { reloadPage(); }
79
+ sendRequest(dokno__base_path + 'article_status', { slug: slug, active: active }, callback, 'POST');
51
80
  }
52
81
 
53
82
  function deleteArticle(id) {
@@ -123,18 +152,6 @@ function toggleVisibility(selector_id) {
123
152
  initIcons();
124
153
  }
125
154
 
126
- function reloadLogs() {
127
- var $log_container = elem('div#dokno-article-log-container');
128
- var category_id = $log_container.getAttribute('data-category-id');
129
- var article_id = $log_container.getAttribute('data-article-id');
130
-
131
- const callback = function(markup) {
132
- elem('div#dokno-article-log-container').innerHTML = markup;
133
- initIcons();
134
- }
135
- sendRequest(dokno__base_path + 'article_log', { category_id: category_id, article_id: article_id }, callback, 'POST');
136
- }
137
-
138
155
  // Pass containers_selector as class name (no prefix)
139
156
  function highlightTerm(terms, containers_selector) {
140
157
  var containers = elems(containers_selector);
@@ -153,3 +170,22 @@ function highlightTerm(terms, containers_selector) {
153
170
  function wrapTermWithHTML(term) {
154
171
  return `<span title="Matching search term" class="dokno-search-term bg-yellow-300 text-gray-900 p-2 rounded mx-1">${term}</span>`
155
172
  }
173
+
174
+ function setReviewForm() {
175
+ const reset_review_date_checkbox = elem('input#reset_review_date');
176
+ const review_notes_textarea = elem('textarea#review_notes');
177
+
178
+ if (!reset_review_date_checkbox) {
179
+ return true;
180
+ }
181
+
182
+ if (reset_review_date_checkbox.checked) {
183
+ review_notes_textarea.removeAttribute('disabled');
184
+ review_notes_textarea.classList.remove('cursor-not-allowed');
185
+ review_notes_textarea.focus();
186
+ } else {
187
+ review_notes_textarea.setAttribute('disabled', 'disabled');
188
+ review_notes_textarea.classList.add('cursor-not-allowed');
189
+ reset_review_date_checkbox.focus();
190
+ }
191
+ }
@@ -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
@@ -8,7 +8,19 @@ module Dokno
8
8
 
9
9
  def show
10
10
  redirect_to root_path if @article.blank?
11
+
11
12
  @search_term = params[:search_term]
13
+ @order = params[:order]
14
+ @category = Category.find_by(code: params[:cat_code].to_s.strip) if params[:cat_code].present?
15
+ @category = @article.categories.first if @category.blank?
16
+
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
12
24
  end
13
25
 
14
26
  def new
@@ -27,7 +39,7 @@ module Dokno
27
39
  set_editor_username
28
40
 
29
41
  if @article.save
30
- flash[:green] = 'Article was created'
42
+ flash[:green] = 'Article was created'
31
43
  @article.categories = Category.where(code: params[:category_code]) if params[:category_code].present?
32
44
  redirect_to article_path @article.slug
33
45
  else
@@ -44,12 +56,14 @@ module Dokno
44
56
  set_editor_username
45
57
 
46
58
  if @article.update(article_params)
47
- flash[:green] = 'Article was updated'
59
+ flash[:green] = 'Article was updated'
48
60
  @article.categories = Category.where(code: params[:category_code])
49
61
  redirect_to article_path @article.slug
50
62
  else
51
- flash.now[:red] = 'Article could not be updated'
52
- @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]
53
67
  render :edit
54
68
  end
55
69
  end
@@ -79,16 +93,10 @@ module Dokno
79
93
  render json: {}, layout: false
80
94
  end
81
95
 
82
- # Ajax-fetched article change log
83
- def article_log
84
- render partial: '/partials/logs',
85
- locals: { article: Article.find_by(id: params[:article_id].to_i) }, layout: false
86
- end
87
-
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
@@ -6,11 +6,14 @@ module Dokno
6
6
  before_action :fetch_category, only: [:index, :edit, :update]
7
7
 
8
8
  def index
9
- @search_term = params[:search_term]
10
- @order = params[:order]&.strip
11
- @order = 'updated' unless %w(updated newest views alpha).include?(@order)
9
+ @search_term = params[:search_term]
10
+ @order = params[:order]&.strip
11
+ @order = 'updated' unless %w(updated newest views alpha).include?(@order)
12
+ @show_up_for_review = can_edit? && !request.path.include?(up_for_review_path)
12
13
 
13
- articles = if @search_term.present?
14
+ articles = if request.path.include? up_for_review_path
15
+ Article.up_for_review(order: @order&.to_sym)
16
+ elsif @search_term.present?
14
17
  Article.search(term: @search_term, category_id: @category&.id, order: @order&.to_sym)
15
18
  elsif @category.present?
16
19
  @category.articles_in_branch(order: @order&.to_sym)
@@ -22,7 +25,7 @@ module Dokno
22
25
  end
23
26
 
24
27
  def new
25
- @category = Category.new
28
+ @category = Category.new
26
29
  @parent_category_code = params[:parent_category_code]
27
30
  end
28
31
 
@@ -38,7 +41,7 @@ module Dokno
38
41
  flash[:green] = 'Category was created'
39
42
  redirect_to article_index_path(@category.code)
40
43
  else
41
- flash.now[:red] = 'Category could not be created'
44
+ flash.now[:red] = 'Category could not be created'
42
45
  @parent_category_code = params[:parent_category_code]
43
46
  render :new
44
47
  end
@@ -51,7 +54,7 @@ module Dokno
51
54
  flash[:green] = 'Category was updated'
52
55
  redirect_to article_index_path(@category.code)
53
56
  else
54
- flash.now[:red] = 'Category could not be updated'
57
+ flash.now[:red] = 'Category could not be updated'
55
58
  @parent_category_code = params[:parent_category_code]
56
59
  render :edit
57
60
  end
@@ -3,11 +3,11 @@ module Dokno
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  def paginate(records, max_per_page: 10)
6
- @page = params[:page].to_i
7
- @total_records = records.size
8
- @total_pages = (@total_records.to_f / max_per_page).ceil
9
- @total_pages = 1 unless @total_pages.positive?
10
- @page = 1 unless @page.positive? && @page <= @total_pages
6
+ @page = params[:page].to_i
7
+ @total_records = records.size
8
+ @total_pages = (@total_records.to_f / max_per_page).ceil
9
+ @total_pages = 1 unless @total_pages.positive?
10
+ @page = 1 unless @page.positive? && @page <= @total_pages
11
11
 
12
12
  records.offset((@page - 1) * max_per_page).limit(max_per_page)
13
13
  end
@@ -8,8 +8,6 @@ module Dokno
8
8
 
9
9
  def user
10
10
  # Attempt to eval the currently signed in 'user' object from the host app
11
- sanitized_user_obj_string = Dokno.config.app_user_object.to_s.split(/\b/).first
12
-
13
11
  proc {
14
12
  $safe = 1
15
13
  eval sanitized_user_obj_string
@@ -19,14 +17,18 @@ module Dokno
19
17
  nil
20
18
  end
21
19
 
20
+ def sanitized_user_obj_string
21
+ Dokno.config.app_user_object.to_s.split(/\b/).first
22
+ end
23
+
22
24
  def username
23
25
  user&.send(Dokno.config.app_user_name_method.to_sym).to_s
24
26
  end
25
27
 
26
28
  def can_edit?
27
29
  # Allow editing by default if host app user object is not configured
28
- return true unless user.present?
29
- return false unless user.respond_to? Dokno.config.app_user_auth_method.to_sym
30
+ return true unless sanitized_user_obj_string.present?
31
+ return false unless user&.respond_to? Dokno.config.app_user_auth_method.to_sym
30
32
 
31
33
  user.send(Dokno.config.app_user_auth_method.to_sym)
32
34
  end
@@ -9,7 +9,7 @@ module Dokno
9
9
 
10
10
  return "Dokno article slug '#{slug}' not found" if article.blank?
11
11
 
12
- %Q(<a href="javascript:;" onclick="doknoOpenPanel('#{j article.slug}');">#{link_text.presence || article.title}</a>).html_safe
12
+ %Q(<a class="dokno-link" href="javascript:;" onclick="doknoOpenPanel('#{j article.slug}');">#{link_text.presence || article.title}</a>).html_safe
13
13
  end
14
14
  end
15
15
  end
@@ -1,5 +1,8 @@
1
1
  module Dokno
2
2
  class ApplicationRecord < ActiveRecord::Base
3
3
  self.abstract_class = true
4
+
5
+ include Engine.routes.url_helpers
6
+ include ActionView::Helpers::DateHelper
4
7
  end
5
8
  end
@@ -3,9 +3,6 @@ require 'redcarpet'
3
3
 
4
4
  module Dokno
5
5
  class Article < ApplicationRecord
6
- include Engine.routes.url_helpers
7
- include ActionView::Helpers::DateHelper
8
-
9
6
  has_and_belongs_to_many :categories
10
7
  has_many :logs, dependent: :destroy
11
8
  has_many :article_slugs, dependent: :destroy
@@ -15,18 +12,28 @@ module Dokno
15
12
  validates :title, length: { in: 5..255 }
16
13
  validate :unique_slug_check
17
14
 
15
+ attr_accessor :editor_username, :reset_review_date, :review_notes
16
+
17
+ before_save :set_review_date, if: :should_set_review_date?
18
18
  before_save :log_changes
19
19
  before_save :track_slug
20
20
 
21
- scope :alpha_order, -> { order(active: :desc, title: :asc) }
22
- scope :view_order, -> { order(active: :desc, views: :desc, title: :asc) }
23
- scope :newest_order, -> { order(active: :desc, created_at: :desc, title: :asc) }
24
- scope :updated_order, -> { order(active: :desc, updated_at: :desc, title: :asc) }
25
-
26
- attr_accessor :editor_username
21
+ scope :active, -> { where(active: true) }
22
+ scope :alpha_order, -> { order(active: :desc, starred: :desc, title: :asc) }
23
+ scope :views_order, -> { order(active: :desc, starred: :desc, views: :desc, title: :asc) }
24
+ scope :newest_order, -> { order(active: :desc, starred: :desc, created_at: :desc, title: :asc) }
25
+ scope :updated_order, -> { order(active: :desc, starred: :desc, updated_at: :desc, title: :asc) }
27
26
 
28
27
  MARKDOWN_PARSER = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
29
28
 
29
+ def review_due_at
30
+ super || (Date.today + 30.years)
31
+ end
32
+
33
+ def markdown
34
+ super || ''
35
+ end
36
+
30
37
  def reading_time
31
38
  minutes_decimal = (("#{summary} #{markdown}".squish.scan(/[\w-]+/).size) / 200.0)
32
39
  approx_minutes = minutes_decimal.ceil
@@ -35,8 +42,22 @@ module Dokno
35
42
  "~ #{approx_minutes} minutes"
36
43
  end
37
44
 
38
- def markdown
39
- super || ''
45
+ def review_due_days
46
+ (review_due_at.to_date - Date.today).to_i
47
+ end
48
+
49
+ def up_for_review?
50
+ active && review_due_days <= Dokno.config.article_review_prompt_days
51
+ end
52
+
53
+ def review_due_days_string
54
+ if review_due_days.positive?
55
+ "This article is up for an accuracy / relevance review in #{review_due_days} #{'day'.pluralize(review_due_days)}"
56
+ elsif review_due_days.negative?
57
+ "This article was up for an accuracy / relevance review #{review_due_days.abs} #{'day'.pluralize(review_due_days)} ago"
58
+ else
59
+ "This article is up for an accuracy / relevance review today"
60
+ end
40
61
  end
41
62
 
42
63
  def markdown_parsed
@@ -52,7 +73,10 @@ module Dokno
52
73
  .where(dokno_articles_categories: { article_id: id })
53
74
  .all
54
75
  .map do |category|
55
- "<a class='underline' href='#{article_index_path(category.code)}?search_term=#{CGI.escape(search_term.to_s)}&order=#{CGI.escape(order.to_s)}'>#{category.name}</a>" if context_category_id != category.id
76
+ next if context_category_id == category.id
77
+
78
+ "<a class='underline' href='#{article_index_path(category.code)}?search_term="\
79
+ "#{CGI.escape(search_term.to_s)}&order=#{CGI.escape(order.to_s)}'>#{category.name}</a>"
56
80
  end.compact
57
81
 
58
82
  return '' if names.blank?
@@ -111,34 +135,24 @@ module Dokno
111
135
  .to_sentence
112
136
  end
113
137
 
114
- # All uncategorized Articles
115
- def self.uncategorized(order: :updated)
138
+ # All articles up for review
139
+ def self.up_for_review(order: :updated)
116
140
  records = Article
117
141
  .includes(:categories_dokno_articles, :categories)
118
- .left_joins(:categories)
119
- .where(active: true, dokno_categories: { id: nil })
120
-
121
- records = records.updated_order if order == :updated
122
- records = records.newest_order if order == :newest
123
- records = records.view_order if order == :views
124
- records = records.alpha_order if order == :alpha
125
-
126
- records
127
- end
142
+ .where(active: true)
143
+ .where('review_due_at <= ?', Date.today + Dokno.config.article_review_prompt_days)
128
144
 
129
- def self.parse_markdown(content)
130
- ActionController::Base.helpers.sanitize(
131
- MARKDOWN_PARSER.render(content),
132
- tags: Dokno.config.tag_whitelist,
133
- attributes: Dokno.config.attr_whitelist
134
- )
145
+ apply_sort(records, order: order)
135
146
  end
136
147
 
137
- def self.template
138
- template_file = File.join(Rails.root, 'config', 'dokno_template.md')
139
- 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 })
140
154
 
141
- File.read(template_file).to_s
155
+ apply_sort(records, order: order)
142
156
  end
143
157
 
144
158
  def self.search(term:, category_id: nil, order: :updated)
@@ -152,10 +166,7 @@ module Dokno
152
166
  .includes(:categories_dokno_articles)
153
167
  .includes(:categories)
154
168
 
155
- records = records.updated_order if order == :updated
156
- records = records.newest_order if order == :newest
157
- records = records.view_order if order == :views
158
- records = records.alpha_order if order == :alpha
169
+ records = apply_sort(records, order: order)
159
170
 
160
171
  return records unless category_id.present?
161
172
 
@@ -169,6 +180,28 @@ module Dokno
169
180
  )
170
181
  end
171
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
+
172
205
  private
173
206
 
174
207
  # Ensure there isn't another Article with the same slug
@@ -188,7 +221,7 @@ module Dokno
188
221
  end
189
222
 
190
223
  def log_changes
191
- return if changes.blank?
224
+ return if changes.blank? && !reset_review_date
192
225
 
193
226
  meta_changes = changes.with_indifferent_access.slice(:slug, :title, :active)
194
227
  content_changes = changes.with_indifferent_access.slice(:summary, :markdown)
@@ -206,10 +239,26 @@ module Dokno
206
239
  content[:after] += values.last.to_s + ' '
207
240
  end
208
241
 
209
- 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
210
259
 
211
- diff = Diffy::SplitDiff.new(content[:before].squish, content[:after].squish, format: :html)
212
- 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
213
262
  end
214
263
  end
215
264
  end