railswiki 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +185 -0
- data/Rakefile +24 -0
- data/app/assets/config/railswiki_manifest.js +2 -0
- data/app/assets/javascripts/railswiki/application.js +13 -0
- data/app/assets/javascripts/railswiki/histories.js +2 -0
- data/app/assets/javascripts/railswiki/invites.js +2 -0
- data/app/assets/javascripts/railswiki/pages.js +2 -0
- data/app/assets/javascripts/railswiki/sessions.js +2 -0
- data/app/assets/javascripts/railswiki/uploaded_files.js +2 -0
- data/app/assets/javascripts/railswiki/users.js +2 -0
- data/app/assets/stylesheets/railswiki/application.css +15 -0
- data/app/assets/stylesheets/railswiki/histories.css +4 -0
- data/app/assets/stylesheets/railswiki/invites.css +4 -0
- data/app/assets/stylesheets/railswiki/pages.scss +55 -0
- data/app/assets/stylesheets/railswiki/sessions.css +4 -0
- data/app/assets/stylesheets/railswiki/uploaded_files.scss +54 -0
- data/app/assets/stylesheets/railswiki/users.css +4 -0
- data/app/controllers/railswiki/application_controller.rb +126 -0
- data/app/controllers/railswiki/histories_controller.rb +48 -0
- data/app/controllers/railswiki/invites_controller.rb +61 -0
- data/app/controllers/railswiki/pages_controller.rb +141 -0
- data/app/controllers/railswiki/sessions_controller.rb +75 -0
- data/app/controllers/railswiki/uploaded_files_controller.rb +100 -0
- data/app/controllers/railswiki/users_controller.rb +55 -0
- data/app/helpers/railswiki/application_helper.rb +26 -0
- data/app/helpers/railswiki/histories_helper.rb +4 -0
- data/app/helpers/railswiki/invites_helper.rb +4 -0
- data/app/helpers/railswiki/pages_helper.rb +76 -0
- data/app/helpers/railswiki/sessions_helper.rb +4 -0
- data/app/helpers/railswiki/title_helper.rb +7 -0
- data/app/helpers/railswiki/uploaded_files_helper.rb +4 -0
- data/app/helpers/railswiki/users_helper.rb +4 -0
- data/app/helpers/railswiki/wiki_helper.rb +189 -0
- data/app/jobs/railswiki/application_job.rb +4 -0
- data/app/mailers/railswiki/application_mailer.rb +6 -0
- data/app/models/railswiki/application_record.rb +5 -0
- data/app/models/railswiki/history.rb +14 -0
- data/app/models/railswiki/invite.rb +20 -0
- data/app/models/railswiki/page.rb +56 -0
- data/app/models/railswiki/uploaded_file.rb +32 -0
- data/app/models/railswiki/user.rb +27 -0
- data/app/uploaders/railswiki/file_uploader.rb +56 -0
- data/app/views/layouts/railswiki/application.html.erb +25 -0
- data/app/views/railswiki/histories/index.html.erb +33 -0
- data/app/views/railswiki/histories/show.html.erb +17 -0
- data/app/views/railswiki/invites/_form.html.erb +38 -0
- data/app/views/railswiki/invites/index.html.erb +39 -0
- data/app/views/railswiki/invites/new.html.erb +7 -0
- data/app/views/railswiki/invites/show.html.erb +34 -0
- data/app/views/railswiki/pages/_form.html.erb +144 -0
- data/app/views/railswiki/pages/edit.html.erb +8 -0
- data/app/views/railswiki/pages/history.html.erb +11 -0
- data/app/views/railswiki/pages/index.html.erb +72 -0
- data/app/views/railswiki/pages/new.html.erb +7 -0
- data/app/views/railswiki/pages/show.html.erb +36 -0
- data/app/views/railswiki/sessions/no_invite.erb +7 -0
- data/app/views/railswiki/sessions/not_authorized.html.erb +12 -0
- data/app/views/railswiki/uploaded_files/_form.html.erb +31 -0
- data/app/views/railswiki/uploaded_files/_inline.html.erb +5 -0
- data/app/views/railswiki/uploaded_files/edit.html.erb +8 -0
- data/app/views/railswiki/uploaded_files/file_dialog.html.erb +11 -0
- data/app/views/railswiki/uploaded_files/image_dialog.html.erb +11 -0
- data/app/views/railswiki/uploaded_files/index.html.erb +36 -0
- data/app/views/railswiki/uploaded_files/new.html.erb +7 -0
- data/app/views/railswiki/uploaded_files/show.html.erb +29 -0
- data/app/views/railswiki/users/_form.html.erb +29 -0
- data/app/views/railswiki/users/edit.html.erb +8 -0
- data/app/views/railswiki/users/index.html.erb +37 -0
- data/app/views/railswiki/users/show.html.erb +59 -0
- data/app/views/shared/_formatting.md.erb +29 -0
- data/app/views/shared/_histories.html.erb +21 -0
- data/app/views/shared/_layout.html.erb +17 -0
- data/app/views/shared/_menu.html.erb +15 -0
- data/app/views/shared/_meta.html.erb +1 -0
- data/app/views/shared/_notices.html.erb +3 -0
- data/app/views/shared/_roles.html.erb +12 -0
- data/app/views/shared/_search.md.erb +5 -0
- data/config/initializers/carrierwave.rb +6 -0
- data/config/initializers/omniauth.rb +16 -0
- data/config/initializers/session_store.rb +1 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20170420000841_create_railswiki_pages.rb +10 -0
- data/db/migrate/20170420010111_add_sessions_table.rb +12 -0
- data/db/migrate/20170420010147_create_railswiki_users.rb +14 -0
- data/db/migrate/20170420021039_add_lowercase_title_to_page.rb +5 -0
- data/db/migrate/20170420021840_create_railswiki_histories.rb +11 -0
- data/db/migrate/20170420235420_add_email_and_image_to_user.rb +8 -0
- data/db/migrate/20170421000333_add_last_login_to_user.rb +5 -0
- data/db/migrate/20170421010945_add_role_to_user.rb +7 -0
- data/db/migrate/20170421020932_create_railswiki_uploaded_files.rb +10 -0
- data/db/migrate/20170421030140_add_title_to_uploaded_file.rb +5 -0
- data/db/migrate/20170517224700_create_railswiki_invites.rb +12 -0
- data/db/migrate/20170517234452_add_role_to_invite.rb +5 -0
- data/db/migrate/20170622033540_set_all_mysql_tables_to_utf8.rb +54 -0
- data/lib/railswiki.rb +5 -0
- data/lib/railswiki/engine.rb +14 -0
- data/lib/railswiki/version.rb +3 -0
- data/lib/tasks/railswiki_tasks.rake +4 -0
- metadata +255 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require_dependency "redcarpet"
|
2
|
+
|
3
|
+
module Railswiki
|
4
|
+
module ApplicationHelper
|
5
|
+
include WikiHelper
|
6
|
+
include TitleHelper
|
7
|
+
|
8
|
+
def time_ago(time)
|
9
|
+
description = if time.present?
|
10
|
+
"#{time_ago_in_words(time)} ago"
|
11
|
+
else
|
12
|
+
"never!"
|
13
|
+
end
|
14
|
+
|
15
|
+
content_tag "span", description, title: "#{time}", class: "time_ago"
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_link(user)
|
19
|
+
if user.present?
|
20
|
+
link_to user.name, user
|
21
|
+
else
|
22
|
+
content_tag "i", "nobody!", class: "invalid_user"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Railswiki
|
2
|
+
module PagesHelper
|
3
|
+
class SpecialPage
|
4
|
+
attr_reader :title, :author, :latest_version
|
5
|
+
|
6
|
+
def initialize(title:)
|
7
|
+
@title = title
|
8
|
+
@author = nil
|
9
|
+
@latest_version = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def builtin_page?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
case title
|
18
|
+
when "Special:Welcome"
|
19
|
+
"Welcome to your new wiki."
|
20
|
+
when "Special:Header"
|
21
|
+
<<-wiki_content
|
22
|
+
[[Home]]
|
23
|
+
{{Special:Search}}
|
24
|
+
wiki_content
|
25
|
+
when "Special:Search"
|
26
|
+
Railswiki::ApplicationController.render(
|
27
|
+
partial: "shared/search",
|
28
|
+
assigns: {},
|
29
|
+
formats: [:md]
|
30
|
+
)
|
31
|
+
when "Special:Footer"
|
32
|
+
<<-wiki_content
|
33
|
+
Copyright {{Special:Year}}
|
34
|
+
wiki_content
|
35
|
+
when "Special:Formatting"
|
36
|
+
Railswiki::ApplicationController.render(
|
37
|
+
partial: "shared/formatting",
|
38
|
+
assigns: {},
|
39
|
+
formats: [:md]
|
40
|
+
)
|
41
|
+
when "Special:Year"
|
42
|
+
"#{Time.now.year}"
|
43
|
+
else
|
44
|
+
"Unknown special page '#{title}'"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def special_page(title)
|
50
|
+
SpecialPage.new({
|
51
|
+
title: "Special:#{title}",
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
def special_pages
|
56
|
+
[
|
57
|
+
special_page("Welcome"),
|
58
|
+
special_page("Header"),
|
59
|
+
special_page("Footer"),
|
60
|
+
special_page("Formatting"),
|
61
|
+
special_page("Search"),
|
62
|
+
special_page("Year")
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_special_page?(page)
|
67
|
+
is_special_page_title?(page.title)
|
68
|
+
end
|
69
|
+
|
70
|
+
def is_special_page_title?(title)
|
71
|
+
return false if title.nil?
|
72
|
+
|
73
|
+
!! special_pages.select { |p| p.title.downcase == title.downcase }.first
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require_dependency "redcarpet"
|
2
|
+
|
3
|
+
module Railswiki
|
4
|
+
module WikiHelper
|
5
|
+
include PagesHelper
|
6
|
+
|
7
|
+
def include_wiki(page_or_title)
|
8
|
+
if page_or_title.is_a?(Page)
|
9
|
+
page = page_or_title
|
10
|
+
else
|
11
|
+
page = select_page(page_or_title)
|
12
|
+
end
|
13
|
+
|
14
|
+
return raw "<!-- no page #{page_or_title} -->" unless page
|
15
|
+
|
16
|
+
wiki_classes = "wiki-included wiki-#{classify_title(page.title)}"
|
17
|
+
|
18
|
+
return raw "<div class=\"#{wiki_classes}\">#{markdown_without_wrap.render page.content}</div>"
|
19
|
+
end
|
20
|
+
|
21
|
+
def include_wiki_class(page)
|
22
|
+
"<div>" + yield + "</div>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def select_page(page_or_title)
|
26
|
+
@select_pages ||= Hash.new do |hash, ref|
|
27
|
+
# Don't try to include blank pages in here
|
28
|
+
return if ref.blank?
|
29
|
+
|
30
|
+
hash[ref] = Page.where(id: ref).
|
31
|
+
or(Page.where(title: [ref, unprettify_title(ref)]).
|
32
|
+
or(Page.where(lowercase_title: [ref.downcase, unprettify_title(ref.downcase), unprettify_slug(ref.downcase)]))).
|
33
|
+
first
|
34
|
+
hash[ref] ||= special_pages.select { |page| page.title.downcase == ref.downcase }.first
|
35
|
+
end
|
36
|
+
@select_pages[page_or_title]
|
37
|
+
end
|
38
|
+
|
39
|
+
def prettify_title(title)
|
40
|
+
title.gsub(/ /, "_")
|
41
|
+
end
|
42
|
+
|
43
|
+
def prettify_slug(title)
|
44
|
+
title.downcase.gsub(/ /, "-")
|
45
|
+
end
|
46
|
+
|
47
|
+
def unprettify_title(title)
|
48
|
+
title.present? ? title.gsub(/_/, " ") : nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def unprettify_slug(title)
|
52
|
+
title.present? ? title.gsub(/-/, " ") : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def classify_title(title)
|
56
|
+
title.gsub(/[^a-z0-9\-_]+/i, "_")
|
57
|
+
end
|
58
|
+
|
59
|
+
def wiki_path(page, options = {})
|
60
|
+
page = "." if page == ""
|
61
|
+
|
62
|
+
if page.respond_to?(:title)
|
63
|
+
title_or_slug_path(page.title, options)
|
64
|
+
else
|
65
|
+
title_or_slug_path(page, options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def title_or_slug_path(title, options)
|
70
|
+
if title == "Home" || title == "home"
|
71
|
+
return Rails.application.routes.url_helpers.root_path(options)
|
72
|
+
end
|
73
|
+
|
74
|
+
if Railswiki::Engine.use_slugs
|
75
|
+
Rails.application.routes.url_helpers.slug_path(prettify_slug(title), options)
|
76
|
+
else
|
77
|
+
Railswiki::Engine.routes.url_helpers.title_path(prettify_title(title), options)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def markdown
|
82
|
+
@markdown ||= Redcarpet::Markdown.new(MarkdownRenderer.new(html_options), markdown_options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def markdown_without_wrap
|
86
|
+
@markdown_without_wrap ||= Redcarpet::Markdown.new(MarkdownRendererWithoutWrap.new(), markdown_options)
|
87
|
+
end
|
88
|
+
|
89
|
+
def markdown_options
|
90
|
+
{
|
91
|
+
tables: true,
|
92
|
+
autolink: true,
|
93
|
+
no_intra_emphasis: true
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def html_options
|
98
|
+
{
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class MarkdownRenderer < Redcarpet::Render::HTML
|
104
|
+
include WikiHelper
|
105
|
+
|
106
|
+
def preprocess(full_document)
|
107
|
+
code_blocks = []
|
108
|
+
|
109
|
+
# Strip out code blocks
|
110
|
+
full_document = full_document.gsub(/(```[^`]+?```)/im) do |match|
|
111
|
+
code_blocks << match
|
112
|
+
"__code_marker:#{code_blocks.length - 1}__"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Include templates {{template}}
|
116
|
+
full_document = full_document.gsub(/\{\{([^\}]+)\}\}/i) do |match|
|
117
|
+
template = select_page($1)
|
118
|
+
if template && template.content
|
119
|
+
# Because we are only including the raw content here, and not
|
120
|
+
# rendering the markdown before including it, it's not possible
|
121
|
+
# to have a stack overflow here. If this ever changes to
|
122
|
+
# somehow call preprocess() again, you MUST add a test to prevent
|
123
|
+
# infinite inclusion.
|
124
|
+
# It also means we don't need to `raw` the output.
|
125
|
+
"#{template.content}"
|
126
|
+
else
|
127
|
+
"*** Unknown template #{$1} ***"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Wiki links [[link|title|extra html]]
|
132
|
+
full_document = full_document.gsub(/\[\[([^\n]+?)\|([^\n]+?)\|([^\n]+?)\]\]/i) do |match|
|
133
|
+
"<a href=\"#{wiki_path($1)}\" #{$3}>#{$2}</a>"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Wiki links [[link|title]]
|
137
|
+
full_document = full_document.gsub(/\[\[([^\n]+?)\|([^\n]+?)\]\]/i) do |match|
|
138
|
+
"[#{$2}](#{wiki_path($1)})"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Wiki links [[link]]
|
142
|
+
full_document = full_document.gsub(/\[\[([^\n]+?)\]\]/i) do |match|
|
143
|
+
"[#{$1}](#{wiki_path($1)})"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Re-insert code blocks
|
147
|
+
full_document = full_document.gsub(/__code_marker:(\d+)__/) do |match|
|
148
|
+
code_blocks[$1.to_i]
|
149
|
+
end
|
150
|
+
|
151
|
+
full_document
|
152
|
+
end
|
153
|
+
|
154
|
+
# Image resizing as per https://github.com/vmg/redcarpet/issues/487
|
155
|
+
def image(link, title, alt_text)
|
156
|
+
if link =~ /^(.+?)\s*=+(\d+)(?:px|)$/
|
157
|
+
# ![alt](url.png =100px)
|
158
|
+
# ![alt](url.png =100)
|
159
|
+
%(<img src="#{$1}" style="max-width: #{$2}px;" alt="#{alt_text}">)
|
160
|
+
elsif link =~ /^(.+?)\s*=+(\d+)(?:px|)x(\d+)(?:px|)$/
|
161
|
+
# ![alt](url.png =30x50)
|
162
|
+
%(<img src="#{$1}" style="max-width: #{$2}px; max-height: #{$3}px;" alt="#{alt_text}">)
|
163
|
+
elsif link =~ /^(.+?)\s*=+(\d+)%$/
|
164
|
+
# ![alt](url.png =50%)
|
165
|
+
%(<img src="#{$1}" style="max-width: #{$2}%;" alt="#{alt_text}">)
|
166
|
+
elsif link =~ /^(.+?)\s*=+(\d+)%x(\d+)%$/
|
167
|
+
# ![alt](url.png =50%x50%)
|
168
|
+
%(<img src="#{$1}" style="max-width: #{$2}%; max-height: #{$3}%;" alt="#{alt_text}">)
|
169
|
+
else
|
170
|
+
%(<img src="#{link}" title="#{title}" alt="#{alt_text}">)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def postprocess(full_document)
|
175
|
+
full_document = full_document.gsub(/<p>= ([^ <>]+)<\/p>/i) do |match|
|
176
|
+
"<#{$1}>"
|
177
|
+
end
|
178
|
+
|
179
|
+
full_document
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Remove the <p> tags around the output as per https://github.com/vmg/redcarpet/issues/92
|
184
|
+
class MarkdownRendererWithoutWrap < MarkdownRenderer
|
185
|
+
def postprocess(full_document)
|
186
|
+
Regexp.new(/\A<p>(.*)<\/p>\Z/m).match(full_document)[1] rescue full_document
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "email_validator"
|
2
|
+
|
3
|
+
module Railswiki
|
4
|
+
class Invite < ApplicationRecord
|
5
|
+
belongs_to :inviting_user, class_name: "User"
|
6
|
+
belongs_to :invited_user, class_name: "User"
|
7
|
+
|
8
|
+
validates :email, presence: true, uniqueness: true, email: true
|
9
|
+
validates :inviting_user, presence: true
|
10
|
+
validates :role, presence: true, inclusion: { in: User::AVAILABLE_ROLES }
|
11
|
+
|
12
|
+
validate :user_cannot_already_be_registered
|
13
|
+
|
14
|
+
def user_cannot_already_be_registered
|
15
|
+
if accepted_at.nil? && User.where(email: email).any?
|
16
|
+
errors.add(:email, "is already registered as a user")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Railswiki
|
2
|
+
class Page < ApplicationRecord
|
3
|
+
attr_accessor :default_content
|
4
|
+
|
5
|
+
has_many :histories, dependent: :destroy
|
6
|
+
|
7
|
+
validates :title, presence: true, uniqueness: true
|
8
|
+
validates :lowercase_title, presence: true, uniqueness: true
|
9
|
+
|
10
|
+
before_validation :save_lowercase_title, on: [:create, :update]
|
11
|
+
validate :lowercase_title_must_equal_title
|
12
|
+
|
13
|
+
delegate :author, to: :latest_version, allow_nil: true
|
14
|
+
|
15
|
+
def content
|
16
|
+
latest_version.present? ? latest_version.body : default_content
|
17
|
+
end
|
18
|
+
|
19
|
+
def latest_version
|
20
|
+
@latest_version ||= histories.where(id: latest_version_id).first
|
21
|
+
end
|
22
|
+
|
23
|
+
def expose_json
|
24
|
+
{
|
25
|
+
id: id,
|
26
|
+
title: title,
|
27
|
+
updated_at: latest_version.created_at,
|
28
|
+
content: content
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def builtin_page?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.search(query)
|
37
|
+
if query
|
38
|
+
where("title LIKE :query COLLATE utf8_general_ci", query: "%#{query}%")
|
39
|
+
else
|
40
|
+
all
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def lowercase_title_must_equal_title
|
47
|
+
if title.downcase != lowercase_title.downcase
|
48
|
+
errors.add(:lowercase_title, "does not match the lowercase title")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def save_lowercase_title
|
53
|
+
self.lowercase_title = self.title.downcase
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_dependency 'carrierwave'
|
2
|
+
require_dependency 'carrierwave/orm/activerecord'
|
3
|
+
|
4
|
+
module Railswiki
|
5
|
+
class UploadedFile < ApplicationRecord
|
6
|
+
mount_uploader :file, FileUploader
|
7
|
+
|
8
|
+
belongs_to :user
|
9
|
+
|
10
|
+
validates :title, presence: true, uniqueness: true
|
11
|
+
validate :file_exists
|
12
|
+
|
13
|
+
delegate :content_type, to: :file
|
14
|
+
|
15
|
+
def file_url
|
16
|
+
file.url.sub "public/", ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_image?
|
20
|
+
file.content_type && MIME::Types[file.content_type].any? &&
|
21
|
+
MIME::Types[file.content_type].first.media_type == "image"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def file_exists
|
27
|
+
if file.file.nil?
|
28
|
+
errors.add(:file, "does not exist")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|