dokno 1.0.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/app/assets/javascripts/dokno.js +79 -27
- data/app/assets/stylesheets/dokno/application.css +1 -1
- data/app/controllers/dokno/application_controller.rb +3 -0
- data/app/controllers/dokno/articles_controller.rb +22 -8
- data/app/controllers/dokno/categories_controller.rb +15 -2
- data/app/controllers/dokno/user_concern.rb +5 -3
- data/app/helpers/dokno/application_helper.rb +1 -3
- data/app/models/dokno/article.rb +87 -38
- data/app/models/dokno/category.rb +39 -15
- data/app/views/dokno/_article_formatting.html.erb +17 -18
- data/app/views/dokno/_article_panel.html.erb +16 -18
- data/app/views/dokno/_panel_formatting.html.erb +47 -57
- data/app/views/dokno/articles/_article_form.html.erb +47 -6
- data/app/views/dokno/articles/show.html.erb +45 -39
- data/app/views/dokno/categories/_category_form.html.erb +6 -1
- data/app/views/dokno/categories/index.html.erb +40 -37
- data/app/views/layouts/dokno/application.html.erb +34 -9
- data/app/views/partials/_category_header.html.erb +29 -0
- data/app/views/partials/_form_errors.html.erb +0 -1
- data/app/views/partials/_logs.html.erb +7 -5
- data/app/views/partials/_pagination.html.erb +20 -18
- data/config/routes.rb +1 -1
- data/db/migrate/20201203190330_baseline.rb +4 -4
- data/db/migrate/20201211192306_add_review_due_at_to_articles.rb +6 -0
- data/db/migrate/20201213165700_add_starred_to_article.rb +5 -0
- data/lib/dokno/config/config.rb +53 -40
- data/lib/dokno/engine.rb +4 -4
- data/lib/dokno/version.rb +1 -1
- data/lib/generators/dokno/templates/config/initializers/dokno.rb +18 -5
- metadata +87 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edd6bc7f282e312599b5d844e48382a5eff63604c13b05d41859c1beb587e878
|
4
|
+
data.tar.gz: b692a98a50654ae4d7d7a77b123289da686ab37131a0bbdf0f4ffc9151b5a1ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
39
|
+
### Configuration
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
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,23 +74,13 @@ function sendRequest(url, data, callback, method) {
|
|
30
74
|
request.send(JSON.stringify(data));
|
31
75
|
}
|
32
76
|
|
33
|
-
function
|
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
|
-
}
|
77
|
+
function deactivateArticle(slug) {
|
78
|
+
const callback = function(_data) { reloadPage(); }
|
40
79
|
sendRequest(dokno__base_path + 'article_status', { slug: slug, active: false }, callback, 'POST');
|
41
80
|
}
|
42
81
|
|
43
|
-
function
|
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
|
-
}
|
82
|
+
function activateArticle(slug) {
|
83
|
+
const callback = function(_data) { reloadPage(); }
|
50
84
|
sendRequest(dokno__base_path + 'article_status', { slug: slug, active: true }, callback, 'POST');
|
51
85
|
}
|
52
86
|
|
@@ -61,6 +95,17 @@ function deleteArticle(id) {
|
|
61
95
|
sendRequest(dokno__base_path + 'articles/' + id, {}, callback, 'DELETE');
|
62
96
|
}
|
63
97
|
|
98
|
+
function deleteCategory(id) {
|
99
|
+
if (!confirm('Delete Category\n\nThis will remove this category. Any articles in this category will become uncategorized and appear on the home page until re-categorized.')) {
|
100
|
+
return true;
|
101
|
+
}
|
102
|
+
|
103
|
+
const callback = function(_data) {
|
104
|
+
location.href = dokno__base_path;
|
105
|
+
}
|
106
|
+
sendRequest(dokno__base_path + 'categories/' + id, {}, callback, 'DELETE');
|
107
|
+
}
|
108
|
+
|
64
109
|
function previewArticleToggle() {
|
65
110
|
const markdown = elem('div#dokno-content-container textarea#markdown').value;
|
66
111
|
const callback = function(data) {
|
@@ -112,18 +157,6 @@ function toggleVisibility(selector_id) {
|
|
112
157
|
initIcons();
|
113
158
|
}
|
114
159
|
|
115
|
-
function reloadLogs() {
|
116
|
-
var $log_container = elem('div#dokno-article-log-container');
|
117
|
-
var category_id = $log_container.getAttribute('data-category-id');
|
118
|
-
var article_id = $log_container.getAttribute('data-article-id');
|
119
|
-
|
120
|
-
const callback = function(markup) {
|
121
|
-
elem('div#dokno-article-log-container').innerHTML = markup;
|
122
|
-
initIcons();
|
123
|
-
}
|
124
|
-
sendRequest(dokno__base_path + 'article_log', { category_id: category_id, article_id: article_id }, callback, 'POST');
|
125
|
-
}
|
126
|
-
|
127
160
|
// Pass containers_selector as class name (no prefix)
|
128
161
|
function highlightTerm(terms, containers_selector) {
|
129
162
|
var containers = elems(containers_selector);
|
@@ -142,3 +175,22 @@ function highlightTerm(terms, containers_selector) {
|
|
142
175
|
function wrapTermWithHTML(term) {
|
143
176
|
return `<span title="Matching search term" class="dokno-search-term bg-yellow-300 text-gray-900 p-2 rounded mx-1">${term}</span>`
|
144
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
|
+
}
|
@@ -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,9 +39,11 @@ module Dokno
|
|
27
39
|
set_editor_username
|
28
40
|
|
29
41
|
if @article.save
|
42
|
+
flash[:green] = 'Article was created'
|
30
43
|
@article.categories = Category.where(code: params[:category_code]) if params[:category_code].present?
|
31
44
|
redirect_to article_path @article.slug
|
32
45
|
else
|
46
|
+
flash.now[:red] = 'Article could not be created'
|
33
47
|
@category_codes = params[:category_code]
|
34
48
|
render :new
|
35
49
|
end
|
@@ -42,16 +56,22 @@ module Dokno
|
|
42
56
|
set_editor_username
|
43
57
|
|
44
58
|
if @article.update(article_params)
|
59
|
+
flash[:green] = 'Article was updated'
|
45
60
|
@article.categories = Category.where(code: params[:category_code])
|
46
61
|
redirect_to article_path @article.slug
|
47
62
|
else
|
48
|
-
|
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]
|
49
67
|
render :edit
|
50
68
|
end
|
51
69
|
end
|
52
70
|
|
53
71
|
def destroy
|
54
72
|
Article.find(params[:id].to_i).destroy!
|
73
|
+
|
74
|
+
flash[:green] = 'Article was deleted'
|
55
75
|
render json: {}, layout: false
|
56
76
|
end
|
57
77
|
|
@@ -73,16 +93,10 @@ module Dokno
|
|
73
93
|
render json: {}, layout: false
|
74
94
|
end
|
75
95
|
|
76
|
-
# Ajax-fetched article change log
|
77
|
-
def article_log
|
78
|
-
render partial: '/partials/logs',
|
79
|
-
locals: { article: Article.find_by(id: params[:article_id].to_i) }, layout: false
|
80
|
-
end
|
81
|
-
|
82
96
|
private
|
83
97
|
|
84
98
|
def article_params
|
85
|
-
params.permit(:slug, :title, :summary, :markdown)
|
99
|
+
params.permit(:slug, :title, :summary, :markdown, :reset_review_date, :review_notes, :starred)
|
86
100
|
end
|
87
101
|
|
88
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
|
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
|
27
|
+
@category = Category.new
|
26
28
|
@parent_category_code = params[:parent_category_code]
|
27
29
|
end
|
28
30
|
|
@@ -35,8 +37,10 @@ module Dokno
|
|
35
37
|
@category = Category.new(name: params[:name], parent: Category.find_by(code: params[:parent_category_code]))
|
36
38
|
|
37
39
|
if @category.save
|
40
|
+
flash[:green] = 'Category was created'
|
38
41
|
redirect_to article_index_path(@category.code)
|
39
42
|
else
|
43
|
+
flash.now[:red] = 'Category could not be created'
|
40
44
|
@parent_category_code = params[:parent_category_code]
|
41
45
|
render :new
|
42
46
|
end
|
@@ -46,13 +50,22 @@ module Dokno
|
|
46
50
|
return redirect_to root_path if @category.blank?
|
47
51
|
|
48
52
|
if @category.update(name: params[:name], parent: Category.find_by(code: params[:parent_category_code]))
|
53
|
+
flash[:green] = 'Category was updated'
|
49
54
|
redirect_to article_index_path(@category.code)
|
50
55
|
else
|
56
|
+
flash.now[:red] = 'Category could not be updated'
|
51
57
|
@parent_category_code = params[:parent_category_code]
|
52
58
|
render :edit
|
53
59
|
end
|
54
60
|
end
|
55
61
|
|
62
|
+
def destroy
|
63
|
+
Category.find(params[:id].to_i).destroy!
|
64
|
+
|
65
|
+
flash[:green] = 'Category was deleted'
|
66
|
+
render json: {}, layout: false
|
67
|
+
end
|
68
|
+
|
56
69
|
private
|
57
70
|
|
58
71
|
def fetch_category
|
@@ -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,13 +17,17 @@ 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
|
30
|
+
return true unless sanitized_user_obj_string.present?
|
29
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)
|
@@ -9,9 +9,7 @@ module Dokno
|
|
9
9
|
|
10
10
|
return "Dokno article slug '#{slug}' not found" if article.blank?
|
11
11
|
|
12
|
-
%Q(
|
13
|
-
<a href="javascript:;" onclick="doknoOpenPanel('#{j article.slug}');">#{link_text.presence || article.title}</a>
|
14
|
-
).html_safe
|
12
|
+
%Q(<a href="javascript:;" onclick="doknoOpenPanel('#{j article.slug}');">#{link_text.presence || article.title}</a>).html_safe
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
data/app/models/dokno/article.rb
CHANGED
@@ -15,18 +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
|
-
scope :
|
22
|
-
scope :
|
23
|
-
scope :
|
24
|
-
scope :
|
25
|
-
|
26
|
-
attr_accessor :editor_username
|
24
|
+
scope :active, -> { where(active: true) }
|
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) }
|
27
29
|
|
28
30
|
MARKDOWN_PARSER = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
29
31
|
|
32
|
+
def review_due_at
|
33
|
+
super || (Date.today + 30.years)
|
34
|
+
end
|
35
|
+
|
36
|
+
def markdown
|
37
|
+
super || ''
|
38
|
+
end
|
39
|
+
|
30
40
|
def reading_time
|
31
41
|
minutes_decimal = (("#{summary} #{markdown}".squish.scan(/[\w-]+/).size) / 200.0)
|
32
42
|
approx_minutes = minutes_decimal.ceil
|
@@ -35,8 +45,22 @@ module Dokno
|
|
35
45
|
"~ #{approx_minutes} minutes"
|
36
46
|
end
|
37
47
|
|
38
|
-
def
|
39
|
-
|
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
|
40
64
|
end
|
41
65
|
|
42
66
|
def markdown_parsed
|
@@ -111,34 +135,24 @@ module Dokno
|
|
111
135
|
.to_sentence
|
112
136
|
end
|
113
137
|
|
114
|
-
# All
|
115
|
-
def self.
|
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
|
-
.
|
119
|
-
.where(
|
142
|
+
.where(active: true)
|
143
|
+
.where('review_due_at <= ?', Date.today + Dokno.config.article_review_prompt_days)
|
120
144
|
|
121
|
-
records
|
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
|
145
|
+
apply_sort(records, order: order)
|
127
146
|
end
|
128
147
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
end
|
136
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
212
|
-
|
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
|