railswiki 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +185 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/config/railswiki_manifest.js +2 -0
  6. data/app/assets/javascripts/railswiki/application.js +13 -0
  7. data/app/assets/javascripts/railswiki/histories.js +2 -0
  8. data/app/assets/javascripts/railswiki/invites.js +2 -0
  9. data/app/assets/javascripts/railswiki/pages.js +2 -0
  10. data/app/assets/javascripts/railswiki/sessions.js +2 -0
  11. data/app/assets/javascripts/railswiki/uploaded_files.js +2 -0
  12. data/app/assets/javascripts/railswiki/users.js +2 -0
  13. data/app/assets/stylesheets/railswiki/application.css +15 -0
  14. data/app/assets/stylesheets/railswiki/histories.css +4 -0
  15. data/app/assets/stylesheets/railswiki/invites.css +4 -0
  16. data/app/assets/stylesheets/railswiki/pages.scss +55 -0
  17. data/app/assets/stylesheets/railswiki/sessions.css +4 -0
  18. data/app/assets/stylesheets/railswiki/uploaded_files.scss +54 -0
  19. data/app/assets/stylesheets/railswiki/users.css +4 -0
  20. data/app/controllers/railswiki/application_controller.rb +126 -0
  21. data/app/controllers/railswiki/histories_controller.rb +48 -0
  22. data/app/controllers/railswiki/invites_controller.rb +61 -0
  23. data/app/controllers/railswiki/pages_controller.rb +141 -0
  24. data/app/controllers/railswiki/sessions_controller.rb +75 -0
  25. data/app/controllers/railswiki/uploaded_files_controller.rb +100 -0
  26. data/app/controllers/railswiki/users_controller.rb +55 -0
  27. data/app/helpers/railswiki/application_helper.rb +26 -0
  28. data/app/helpers/railswiki/histories_helper.rb +4 -0
  29. data/app/helpers/railswiki/invites_helper.rb +4 -0
  30. data/app/helpers/railswiki/pages_helper.rb +76 -0
  31. data/app/helpers/railswiki/sessions_helper.rb +4 -0
  32. data/app/helpers/railswiki/title_helper.rb +7 -0
  33. data/app/helpers/railswiki/uploaded_files_helper.rb +4 -0
  34. data/app/helpers/railswiki/users_helper.rb +4 -0
  35. data/app/helpers/railswiki/wiki_helper.rb +189 -0
  36. data/app/jobs/railswiki/application_job.rb +4 -0
  37. data/app/mailers/railswiki/application_mailer.rb +6 -0
  38. data/app/models/railswiki/application_record.rb +5 -0
  39. data/app/models/railswiki/history.rb +14 -0
  40. data/app/models/railswiki/invite.rb +20 -0
  41. data/app/models/railswiki/page.rb +56 -0
  42. data/app/models/railswiki/uploaded_file.rb +32 -0
  43. data/app/models/railswiki/user.rb +27 -0
  44. data/app/uploaders/railswiki/file_uploader.rb +56 -0
  45. data/app/views/layouts/railswiki/application.html.erb +25 -0
  46. data/app/views/railswiki/histories/index.html.erb +33 -0
  47. data/app/views/railswiki/histories/show.html.erb +17 -0
  48. data/app/views/railswiki/invites/_form.html.erb +38 -0
  49. data/app/views/railswiki/invites/index.html.erb +39 -0
  50. data/app/views/railswiki/invites/new.html.erb +7 -0
  51. data/app/views/railswiki/invites/show.html.erb +34 -0
  52. data/app/views/railswiki/pages/_form.html.erb +144 -0
  53. data/app/views/railswiki/pages/edit.html.erb +8 -0
  54. data/app/views/railswiki/pages/history.html.erb +11 -0
  55. data/app/views/railswiki/pages/index.html.erb +72 -0
  56. data/app/views/railswiki/pages/new.html.erb +7 -0
  57. data/app/views/railswiki/pages/show.html.erb +36 -0
  58. data/app/views/railswiki/sessions/no_invite.erb +7 -0
  59. data/app/views/railswiki/sessions/not_authorized.html.erb +12 -0
  60. data/app/views/railswiki/uploaded_files/_form.html.erb +31 -0
  61. data/app/views/railswiki/uploaded_files/_inline.html.erb +5 -0
  62. data/app/views/railswiki/uploaded_files/edit.html.erb +8 -0
  63. data/app/views/railswiki/uploaded_files/file_dialog.html.erb +11 -0
  64. data/app/views/railswiki/uploaded_files/image_dialog.html.erb +11 -0
  65. data/app/views/railswiki/uploaded_files/index.html.erb +36 -0
  66. data/app/views/railswiki/uploaded_files/new.html.erb +7 -0
  67. data/app/views/railswiki/uploaded_files/show.html.erb +29 -0
  68. data/app/views/railswiki/users/_form.html.erb +29 -0
  69. data/app/views/railswiki/users/edit.html.erb +8 -0
  70. data/app/views/railswiki/users/index.html.erb +37 -0
  71. data/app/views/railswiki/users/show.html.erb +59 -0
  72. data/app/views/shared/_formatting.md.erb +29 -0
  73. data/app/views/shared/_histories.html.erb +21 -0
  74. data/app/views/shared/_layout.html.erb +17 -0
  75. data/app/views/shared/_menu.html.erb +15 -0
  76. data/app/views/shared/_meta.html.erb +1 -0
  77. data/app/views/shared/_notices.html.erb +3 -0
  78. data/app/views/shared/_roles.html.erb +12 -0
  79. data/app/views/shared/_search.md.erb +5 -0
  80. data/config/initializers/carrierwave.rb +6 -0
  81. data/config/initializers/omniauth.rb +16 -0
  82. data/config/initializers/session_store.rb +1 -0
  83. data/config/routes.rb +25 -0
  84. data/db/migrate/20170420000841_create_railswiki_pages.rb +10 -0
  85. data/db/migrate/20170420010111_add_sessions_table.rb +12 -0
  86. data/db/migrate/20170420010147_create_railswiki_users.rb +14 -0
  87. data/db/migrate/20170420021039_add_lowercase_title_to_page.rb +5 -0
  88. data/db/migrate/20170420021840_create_railswiki_histories.rb +11 -0
  89. data/db/migrate/20170420235420_add_email_and_image_to_user.rb +8 -0
  90. data/db/migrate/20170421000333_add_last_login_to_user.rb +5 -0
  91. data/db/migrate/20170421010945_add_role_to_user.rb +7 -0
  92. data/db/migrate/20170421020932_create_railswiki_uploaded_files.rb +10 -0
  93. data/db/migrate/20170421030140_add_title_to_uploaded_file.rb +5 -0
  94. data/db/migrate/20170517224700_create_railswiki_invites.rb +12 -0
  95. data/db/migrate/20170517234452_add_role_to_invite.rb +5 -0
  96. data/db/migrate/20170622033540_set_all_mysql_tables_to_utf8.rb +54 -0
  97. data/lib/railswiki.rb +5 -0
  98. data/lib/railswiki/engine.rb +14 -0
  99. data/lib/railswiki/version.rb +3 -0
  100. data/lib/tasks/railswiki_tasks.rake +4 -0
  101. 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,4 @@
1
+ module Railswiki
2
+ module HistoriesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Railswiki
2
+ module InvitesHelper
3
+ end
4
+ 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,4 @@
1
+ module Railswiki
2
+ module SessionsHelper
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Railswiki
2
+ module TitleHelper
3
+ def title(page_title)
4
+ content_for(:title) { page_title.join(" - ") }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module Railswiki
2
+ module UploadedFilesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Railswiki
2
+ module UsersHelper
3
+ end
4
+ 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,4 @@
1
+ module Railswiki
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Railswiki
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Railswiki
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module Railswiki
2
+ class History < ApplicationRecord
3
+ belongs_to :page
4
+ belongs_to :author, class_name: "User"
5
+
6
+ def expose_json
7
+ {
8
+ id: id,
9
+ updated_at: created_at,
10
+ content: body
11
+ }
12
+ end
13
+ end
14
+ 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