dokno 1.2.1 → 1.4.4

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: 3013390f5b81b28c13f180afb373c4fdfb50b2c4b7e4a7cfd7bed7e176c94dca
4
+ data.tar.gz: 9a150f57d2551eb9d46467e661a108b036e0df91924d4d77798f83ce85bc2e29
5
5
  SHA512:
6
- metadata.gz: b58630386a83f001e2e3a49def7e14cb833de066724be74b44ca56b08494ff9a72994ea0c2e5f324b09832a2f8848921fd9fa1eb1a41e83f11e8aba166669438
7
- data.tar.gz: 6f1cf01dcd513f4dc4ce57acc85ba7dcb42aaac06585431562bdf06c5f9c012e0ea8a0c7588974045ca1e14704e9d2630a68150c000a032e321bf0c70a61f0bd
6
+ metadata.gz: 116c2f347fb8e045f508e490eaf6df3981caea39ad6fd3a200a02272ace62c99acf08701f5bc90398a0ca3c8397a8d391d0d79de57c22404235862152e2bdd7b
7
+ data.tar.gz: 4b980ae70724dc2bf54c59fef7eb99bdeeb4c08700cd90d9fdf0b0c1f3e1726a7b6436328be98942df8e9509ab8fb21ef78d8bd3f95e86a028cb23b7b2155da0
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
 
@@ -46,7 +46,7 @@ function applyCategoryCriteria(category_code, term, order) {
46
46
 
47
47
  function goToPage(url) {
48
48
  var param_join = url.indexOf('?') >= 0 ? '&' : '?';
49
- location.href=url + param_join + '_=' + Math.round(new Date().getTime());
49
+ location.href = url + param_join + '_=' + Math.round(new Date().getTime());
50
50
  }
51
51
 
52
52
  function reloadPage() {
@@ -74,14 +74,9 @@ function sendRequest(url, data, callback, method) {
74
74
  request.send(JSON.stringify(data));
75
75
  }
76
76
 
77
- function deactivateArticle(slug) {
77
+ function setArticleStatus(slug, active) {
78
78
  const callback = function(_data) { reloadPage(); }
79
- sendRequest(dokno__base_path + 'article_status', { slug: slug, active: false }, callback, 'POST');
80
- }
81
-
82
- function activateArticle(slug) {
83
- const callback = function(_data) { reloadPage(); }
84
- sendRequest(dokno__base_path + 'article_status', { slug: slug, active: true }, callback, 'POST');
79
+ sendRequest(dokno__base_path + 'article_status', { slug: slug, active: active }, callback, 'POST');
85
80
  }
86
81
 
87
82
  function deleteArticle(id) {
@@ -175,3 +170,22 @@ function highlightTerm(terms, containers_selector) {
175
170
  function wrapTermWithHTML(term) {
176
171
  return `<span title="Matching search term" class="dokno-search-term bg-yellow-300 text-gray-900 p-2 rounded mx-1">${term}</span>`
177
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
@@ -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
@@ -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
@@ -28,7 +28,7 @@ module Dokno
28
28
  def can_edit?
29
29
  # Allow editing by default if host app user object is not configured
30
30
  return true unless sanitized_user_obj_string.present?
31
- return false unless user.respond_to? Dokno.config.app_user_auth_method.to_sym
31
+ return false unless user&.respond_to? Dokno.config.app_user_auth_method.to_sym
32
32
 
33
33
  user.send(Dokno.config.app_user_auth_method.to_sym)
34
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,19 +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
21
  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
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) }
28
26
 
29
27
  MARKDOWN_PARSER = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
30
28
 
29
+ def review_due_at
30
+ super || (Date.today + 30.years)
31
+ end
32
+
33
+ def markdown
34
+ super || ''
35
+ end
36
+
31
37
  def reading_time
32
38
  minutes_decimal = (("#{summary} #{markdown}".squish.scan(/[\w-]+/).size) / 200.0)
33
39
  approx_minutes = minutes_decimal.ceil
@@ -36,8 +42,22 @@ module Dokno
36
42
  "~ #{approx_minutes} minutes"
37
43
  end
38
44
 
39
- def markdown
40
- 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
41
61
  end
42
62
 
43
63
  def markdown_parsed
@@ -53,7 +73,10 @@ module Dokno
53
73
  .where(dokno_articles_categories: { article_id: id })
54
74
  .all
55
75
  .map do |category|
56
- "<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>"
57
80
  end.compact
58
81
 
59
82
  return '' if names.blank?
@@ -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 })
121
-
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
128
- end
142
+ .where(active: true)
143
+ .where('review_due_at <= ?', Date.today + Dokno.config.article_review_prompt_days)
129
144
 
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
- )
145
+ apply_sort(records, order: order)
136
146
  end
137
147
 
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