railswiki 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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