cadmus 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.yardopts +3 -0
- data/Gemfile +8 -0
- data/LICENSE +7 -0
- data/README.md +241 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/cadmus.previewablehtml.js +22 -0
- data/app/views/cadmus/pages/_form.html.erb +44 -0
- data/app/views/cadmus/pages/edit.html.erb +8 -0
- data/app/views/cadmus/pages/index.html.erb +17 -0
- data/app/views/cadmus/pages/new.html.erb +7 -0
- data/app/views/cadmus/pages/show.html.erb +1 -0
- data/cadmus.gemspec +20 -0
- data/lib/cadmus/controller_extensions.rb +221 -0
- data/lib/cadmus/markdown.rb +124 -0
- data/lib/cadmus/page.rb +53 -0
- data/lib/cadmus/renderers.rb +122 -0
- data/lib/cadmus/routing.rb +69 -0
- data/lib/cadmus/slugs.rb +83 -0
- data/lib/cadmus/version.rb +3 -0
- data/lib/cadmus.rb +12 -0
- metadata +93 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2011-2012 Gively, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
# Cadmus: an embeddable CMS for Rails
|
2
|
+
|
3
|
+
Cadmus is an embeddable content management system for Rails 3 applications. It's based on [Liquid](http://liquidmarkup.org)
|
4
|
+
and is designed to be small and unobtrusive.
|
5
|
+
|
6
|
+
Cadmus doesn't define controllers or models itself, but rather, provides mixins to add CMS-like functionality to controllers
|
7
|
+
and models you create. This allows a great deal of customization. For example, Cadmus doesn't provide any user authentication
|
8
|
+
or authorization functionality, but because it hooks into controllers in your app, you can add virtually any authorization
|
9
|
+
system you want.
|
10
|
+
|
11
|
+
Similarly, Cadmus doesn't provide a Page model, but rather, a mixin for creating page-like models. This theoretically allows
|
12
|
+
you to add functionality to your Page objects, include multiple different page-like models, or use any ActiveModel-compatible
|
13
|
+
ORM you want instead of ActiveRecord.
|
14
|
+
|
15
|
+
One additional feature is the ability for pages to have parents. A parent can be any model object. Page parent objects allow
|
16
|
+
you to create separate "sections" of your site - for example, if you have a project-hosting application that includes multiple
|
17
|
+
projects, each of which has its own separate space of CMS pages. (Page parents aren't intended for creating sub-pages -
|
18
|
+
instead, just use forward-slash characters in the page slug to simulate folders, and Cadmus will handle it.)
|
19
|
+
|
20
|
+
## Basic Installation
|
21
|
+
|
22
|
+
First, add Cadmus to your Gemfile:
|
23
|
+
|
24
|
+
gem 'cadmus'
|
25
|
+
gem 'redcarpet' # (required only if you intend to use Cadmus' Markdown support)
|
26
|
+
|
27
|
+
The next step is to create a Page model. Your app can have multiple Page models if you like, but for this example, we'll just
|
28
|
+
create one.
|
29
|
+
|
30
|
+
rails generate model Page name:text slug:string content:text parent_id:integer parent_type:string
|
31
|
+
|
32
|
+
You'll need to tweak the generated migration and model slightly. In the migration, after the `create_pages` block, add a
|
33
|
+
unique index on the parent and slug columns:
|
34
|
+
|
35
|
+
add_index :pages, [:parent_type, :parent_id, :slug], :unique => true
|
36
|
+
|
37
|
+
And in the model, add a `cadmus_page` declaration:
|
38
|
+
|
39
|
+
class Page < ActiveRecord::Base
|
40
|
+
cadmus_page
|
41
|
+
end
|
42
|
+
|
43
|
+
You'll need a controller to deal with your pages. Here's a minimal example of one:
|
44
|
+
|
45
|
+
class PagesController < ApplicationController
|
46
|
+
include Cadmus::PagesController
|
47
|
+
|
48
|
+
protected
|
49
|
+
def page_class
|
50
|
+
Page
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
`Cadmus::PagesController` automatically adds the seven RESTful resource methods to your controller. It requires that you
|
55
|
+
define a `page_class` method that returns the class for pages it's dealing with. (This could potentially return different
|
56
|
+
classes depending on request parameters, if you need it to - or, you could also set up different controllers for different
|
57
|
+
types of page.)
|
58
|
+
|
59
|
+
Finally, you'll need to create routes for this controller. Cadmus provides a built-in helper for that:
|
60
|
+
|
61
|
+
MyApp::Application.routes.draw do
|
62
|
+
cadmus_pages
|
63
|
+
end
|
64
|
+
|
65
|
+
This will create the following routes:
|
66
|
+
|
67
|
+
* GET /pages => PagesController#index
|
68
|
+
* GET /pages/new => PagesController#new
|
69
|
+
* POST /pages => PagesController#create
|
70
|
+
* GET /pages/slug => PagesController#show
|
71
|
+
* PUT /pages/slug => PagesController#update
|
72
|
+
* DELETE /pages/slug => PagesController#destroy
|
73
|
+
|
74
|
+
## Authorization Control
|
75
|
+
|
76
|
+
The pages controller is where you'll need to hook into any authorization or authentication system your app might use.
|
77
|
+
We use CanCan, so here's an example of how we do that:
|
78
|
+
|
79
|
+
class PagesController < ApplicationController
|
80
|
+
include Cadmus::PagesController
|
81
|
+
|
82
|
+
authorize_resource :page
|
83
|
+
|
84
|
+
protected
|
85
|
+
def page_class
|
86
|
+
Page
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Ability
|
91
|
+
def initialize(user)
|
92
|
+
can :read, Page
|
93
|
+
return unless user
|
94
|
+
|
95
|
+
# in this example, we've added an owner_id column to our Page model
|
96
|
+
can :manage, Page, :owner_id => user.id
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Easy-peasy. You can use other authorization plugins in a similar way - with Cadmus, you control the CMS models,
|
101
|
+
controllers and routes, so you can add whatever code is appropriate for your app.
|
102
|
+
|
103
|
+
## Pages With Parents
|
104
|
+
|
105
|
+
Suppose you've got an app that hosts web sites for local baseball teams. Your app lets the teams manage their own
|
106
|
+
sites, and do stuff like add their team logo, uniform colors, roster, etc. Now you'd like to let them add custom
|
107
|
+
content pages as well.
|
108
|
+
|
109
|
+
You already have the following routes set up in your routes.rb file:
|
110
|
+
|
111
|
+
DugoutCoach::Application.routes.draw do
|
112
|
+
resources :teams do
|
113
|
+
resources :players
|
114
|
+
resources :schedule
|
115
|
+
end
|
116
|
+
|
117
|
+
cadmus_pages # for global pages on your site
|
118
|
+
end
|
119
|
+
|
120
|
+
So, for example, the URL for the Cambridge Cosmonauts might be http://dugoutcoach.net/teams/cosmonauts. They also
|
121
|
+
have http://dugoutcoach.net/teams/cosmonauts/players and http://dugoutcoach.net/teams/cosmonauts/schedule.
|
122
|
+
|
123
|
+
You can add a "pages" namespace pretty easily:
|
124
|
+
|
125
|
+
DugoutCoach::Application.routes.draw do
|
126
|
+
resources :teams do
|
127
|
+
resources :players
|
128
|
+
resources :schedule
|
129
|
+
cadmus_pages :controller => :team_pages
|
130
|
+
end
|
131
|
+
|
132
|
+
cadmus_pages
|
133
|
+
end
|
134
|
+
|
135
|
+
Now you have a way of separating team-specific pages from global pages on the site. The URLs for these pages might be,
|
136
|
+
for example, http://dugoutcoach.net/teams/cosmonauts/directions, or
|
137
|
+
http://dugoutcoach.net/teams/cosmonauts/promotions/free-hat-day (remember, Cadmus slugs can contain slashes). We'll
|
138
|
+
now need a TeamPages controller to handle these:
|
139
|
+
|
140
|
+
class TeamPagesController < ApplicationController
|
141
|
+
include Cadmus::PagesController
|
142
|
+
|
143
|
+
self.page_parent_class = Team # page's parent is a Team
|
144
|
+
self.page_parent_name = "team" # parent ID is in params[:team_id]
|
145
|
+
self.find_parent_by = "slug" # parent ID is the Team's "slug" field rather than "id"
|
146
|
+
|
147
|
+
authorize_resource :page
|
148
|
+
|
149
|
+
protected
|
150
|
+
def page_class
|
151
|
+
Page
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
Note that for this example, we've kept the same `Page` class for both controllers. We could have also created a
|
156
|
+
separate `TeamPage` model, but that's not required.
|
157
|
+
|
158
|
+
### Shallow Page URLs
|
159
|
+
|
160
|
+
The Cambridge Cosmonauts are unhappy! Their URLs are too long. Why should the pages in their team site have a "/pages/"
|
161
|
+
in them just because they created them themselves?
|
162
|
+
|
163
|
+
Chill out, Cosmonauts. Cadmus makes it easy:
|
164
|
+
|
165
|
+
DugoutCoach::Application.routes.draw do
|
166
|
+
resources :teams do
|
167
|
+
resources :players
|
168
|
+
resources :schedule
|
169
|
+
cadmus_pages :controller => :team_pages, :shallow => true
|
170
|
+
end
|
171
|
+
|
172
|
+
cadmus_pages
|
173
|
+
end
|
174
|
+
|
175
|
+
Now the PagesController's `show`, `edit`, `update`, and `destroy` actions don't use the "/pages/" part of the URL. The
|
176
|
+
URLs now look like this:
|
177
|
+
|
178
|
+
* GET /teams/cosmonauts/pages => PagesController#index
|
179
|
+
* GET /teams/cosmonauts/pages/new => PagesController#new
|
180
|
+
* POST /teams/cosmonauts/pages => PagesController#create
|
181
|
+
* GET /teams/cosmonauts/page-slug => PagesController#show
|
182
|
+
* GET /teams/cosmonauts/page-slug/edit => PagesController#edit
|
183
|
+
* PUT /teams/cosmonauts/page-slug => PagesController#update
|
184
|
+
* DELETE /teams/cosmonauts/page-slug => PagesController#destroy
|
185
|
+
|
186
|
+
When you use shallow page URLs, it's important to put the `cadmus_pages` declaration as the last one in the block,
|
187
|
+
because it's going to put a path-globbing wildcard in the scope from which it's called. Thus, it should be the
|
188
|
+
lowest-priority route in its context.
|
189
|
+
|
190
|
+
## Liquid Variables
|
191
|
+
|
192
|
+
The Cambridge Cosmonauts have a policy of changing their uniform color on a weekly basis. Why? I don't know. Go
|
193
|
+
Cosmonauts!
|
194
|
+
|
195
|
+
Needless to say, they don't want to go editing every single page where they mention that. Fortunately, you can
|
196
|
+
help them by providing them with a Liquid template variable they can use like so:
|
197
|
+
|
198
|
+
```html
|
199
|
+
<h1>We're the Cosmonauts!</h1>
|
200
|
+
|
201
|
+
<p>Our uniform color this week is {{ team.uniform_color }}!</p>
|
202
|
+
```
|
203
|
+
|
204
|
+
To do this, you'll need to expose `team` as a Liquid assign variable:
|
205
|
+
|
206
|
+
class TeamPagesController < ApplicationController
|
207
|
+
include Cadmus::PagesController
|
208
|
+
|
209
|
+
self.page_parent_class = Team # page's parent is a Team
|
210
|
+
self.page_parent_name = "team" # parent ID is in params[:team_id]
|
211
|
+
self.find_parent_by = "slug" # parent ID is the Team's "slug" field rather than "id"
|
212
|
+
|
213
|
+
authorize_resource :page
|
214
|
+
|
215
|
+
protected
|
216
|
+
def page_class
|
217
|
+
Page
|
218
|
+
end
|
219
|
+
|
220
|
+
def liquid_assigns
|
221
|
+
{ :team => @page.parent }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
Defining a `liquid_assigns` method will cause Cadmus to use the return value of that method as the Liquid assigns hash.
|
226
|
+
(Similarly, you can define `liquid_filters` and `liquid_registers` methods that do what they say on the tin.)
|
227
|
+
|
228
|
+
You'll also need to make your Team model usable from Liquid. The simplest way to do that is using `liquid_methods`:
|
229
|
+
|
230
|
+
class Team < ActiveRecord::Base
|
231
|
+
liquid_methods :name, :uniform_color
|
232
|
+
|
233
|
+
# everything else in your model...
|
234
|
+
end
|
235
|
+
|
236
|
+
You could also define a `to_liquid` method that returns a `Liquid::Drop` subclass for Teams, if you need to do things
|
237
|
+
more complicated than just return data values.
|
238
|
+
|
239
|
+
## Copyright and Licensing
|
240
|
+
|
241
|
+
Copyright © 2011-2012 Gively, Inc. Cadmus is released under the MIT license. For more information, see the LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(function($) {
|
2
|
+
$.fn.cadmusPreviewableHtml = function() {
|
3
|
+
this.each(function() {
|
4
|
+
var textarea = this;
|
5
|
+
var $this = $(this);
|
6
|
+
var $previewIn = $("#" + $this.attr('data-preview-in'));
|
7
|
+
|
8
|
+
$this.on('cadmus.htmlChange', function(){
|
9
|
+
$previewIn.html($this.val());
|
10
|
+
});
|
11
|
+
|
12
|
+
textarea.cadmusTextValue = "";
|
13
|
+
setInterval(function() {
|
14
|
+
var newCadmusTextValue = $this.val();
|
15
|
+
if (textarea.cadmusTextValue != newCadmusTextValue) {
|
16
|
+
textarea.cadmusTextValue = newCadmusTextValue;
|
17
|
+
$this.trigger('cadmus.htmlChange');
|
18
|
+
}
|
19
|
+
}, 100);
|
20
|
+
});
|
21
|
+
};
|
22
|
+
})(jQuery);
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<% if @page.errors.any? %>
|
2
|
+
<div id="error_explanation">
|
3
|
+
<h2><%= pluralize(@page.errors.count, "error") %> prohibited this page from being saved:</h2>
|
4
|
+
|
5
|
+
<ul>
|
6
|
+
<% @page.errors.full_messages.each do |msg| %>
|
7
|
+
<li><%= msg %></li>
|
8
|
+
<% end %>
|
9
|
+
</ul>
|
10
|
+
</div>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<div class="field">
|
14
|
+
<%= f.label :name %>
|
15
|
+
<%= f.text_field :name %>
|
16
|
+
</div>
|
17
|
+
<div class="field">
|
18
|
+
<%= f.label :slug, "URL" %>
|
19
|
+
<p>
|
20
|
+
<%= url_for(:action => 'index', :only_path => false).sub(/pages$/, '') %>
|
21
|
+
<%= f.text_field :slug %>
|
22
|
+
</p>
|
23
|
+
</div>
|
24
|
+
<p>Type html in the box below and see a preview right under it</p>
|
25
|
+
<div class="field">
|
26
|
+
<%= f.label :content %>
|
27
|
+
<%= f.text_area :content, :style => "font-family: monospace; width: 60em;",
|
28
|
+
:class => "cadmus-previewable-html", :"data-preview-in" => "html-preview" %>
|
29
|
+
</div>
|
30
|
+
<div class="actions">
|
31
|
+
<%= f.submit %>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<%= javascript_include_tag "cadmus.previewablehtml.js" %>
|
35
|
+
<script type="text/javascript">
|
36
|
+
$(document).ready(function(){
|
37
|
+
$('.cadmus-previewable-html').cadmusPreviewableHtml();
|
38
|
+
});
|
39
|
+
</script>
|
40
|
+
<p><b>Rendered html preview</b></p>
|
41
|
+
<div id="html-preview" style="border:1px solid;">
|
42
|
+
</div>
|
43
|
+
<br/><br/>
|
44
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<h1>Editing page "<%= @page.name %>"</h1>
|
2
|
+
|
3
|
+
<%= form_for(@page, :url => { :action => :update, :page_glob => @page.slug }, :class => "edit_cadmus_page") do |f| %>
|
4
|
+
<%= render :partial => 'cadmus/pages/form', :locals => { :f => f } %>
|
5
|
+
<% end -%>
|
6
|
+
|
7
|
+
<%= link_to 'Show', { :action => 'show', :page_glob => @page.slug } %> |
|
8
|
+
<%= link_to 'Back', { :action => 'index' } %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<h1>
|
2
|
+
<% if @page_parent && @page_parent.respond_to?(:name) -%>
|
3
|
+
<%= @page_parent.name %>
|
4
|
+
<% else -%>
|
5
|
+
Pages
|
6
|
+
<% end -%>
|
7
|
+
</h1>
|
8
|
+
|
9
|
+
<ul class="cadmus_page_list">
|
10
|
+
<% @pages.each do |page| %>
|
11
|
+
<li><%= link_to page.name, { :action => 'show', :page_glob => page.slug } %></li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
|
15
|
+
<br />
|
16
|
+
|
17
|
+
<%= link_to 'New Page', :action => 'new' %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= cadmus_renderer.render(@page.liquid_template, :html) %>
|
data/cadmus.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/cadmus/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Nat Budin", "Aziz Khoury"]
|
6
|
+
gem.email = ["natbudin@gmail.com", "bentael@gmail.com"]
|
7
|
+
gem.description = %q{Why deal with setting up a separate CMS? Cadmus is just a little bit of CMS and fits nicely into your existing app. It can be used for allowing users to customize areas of the site, for creating editable "about us" pages, and more.}
|
8
|
+
gem.summary = %q{Embeddable CMS for Rails 3 apps}
|
9
|
+
gem.homepage = "http://github.com/gively/cadmus"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "cadmus"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Cadmus::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("rails", ">= 3.0.0")
|
19
|
+
gem.add_dependency("liquid")
|
20
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module Cadmus
|
2
|
+
|
3
|
+
# A controller mixin that includes all the RESTful resource actions for viewing and administering Cadmus
|
4
|
+
# pages. This mixin provides a great deal of flexibility for customizing the behavior of the resulting
|
5
|
+
# controller.
|
6
|
+
#
|
7
|
+
# Controllers that include this mixin are expected to define at least a page_class method, which returns
|
8
|
+
# the class to be used for pages in this context.
|
9
|
+
#
|
10
|
+
# == Examples
|
11
|
+
#
|
12
|
+
# A basic PagesController for a site:
|
13
|
+
#
|
14
|
+
# class PagesController < ApplicationController
|
15
|
+
# include Cadmus::PagesController
|
16
|
+
#
|
17
|
+
# # no action methods are necessary because PagesController defines them for us
|
18
|
+
#
|
19
|
+
# protected
|
20
|
+
# def page_class
|
21
|
+
# Page
|
22
|
+
# end
|
23
|
+
# # Page must be defined as a model that includes Cadmus::Page
|
24
|
+
#
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# A PagesController using CanCan for authorization control:
|
28
|
+
#
|
29
|
+
# class PagesController < ApplicationController
|
30
|
+
# include Cadmus::PagesController
|
31
|
+
#
|
32
|
+
# # no need for load_resource because PagesController does that for us
|
33
|
+
# authorize_resource :page
|
34
|
+
#
|
35
|
+
# protected
|
36
|
+
# def page_class
|
37
|
+
# Page
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# A controller for pages inside a Book object. This controller uses URLs of the form
|
42
|
+
# +/books/:book_id/pages/...+ The book_id parameter is a slug, not an ID. First, here's
|
43
|
+
# the Book model:
|
44
|
+
#
|
45
|
+
# class Book < ActiveRecord::Base
|
46
|
+
# # This association has to be called "pages" because that's what BookPagesController
|
47
|
+
# # will expect to use to find them.
|
48
|
+
# has_many :pages, :class_name => "BookPage"
|
49
|
+
#
|
50
|
+
# validates_uniqueness_of :slug
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# class BookPagesController < ApplicationController
|
54
|
+
# include Cadmus::PagesController
|
55
|
+
#
|
56
|
+
# self.page_parent_class = Book # pages must have a Book as their parent
|
57
|
+
# self.page_parent_name = "book" # use params[:book_id] for finding Books
|
58
|
+
# self.find_parent_by = "slug" # use the Book's slug field for finding Books
|
59
|
+
#
|
60
|
+
# protected
|
61
|
+
# def page_class
|
62
|
+
# BookPage
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
module PagesController
|
66
|
+
extend ActiveSupport::Concern
|
67
|
+
include Cadmus::Renderable
|
68
|
+
|
69
|
+
included do
|
70
|
+
class << self
|
71
|
+
attr_accessor :page_parent_name, :page_parent_class, :find_parent_by
|
72
|
+
end
|
73
|
+
|
74
|
+
before_filter :load_parent_and_page
|
75
|
+
helper_method :cadmus_renderer
|
76
|
+
end
|
77
|
+
|
78
|
+
def index
|
79
|
+
@pages = page_scope.order(:name).all
|
80
|
+
|
81
|
+
respond_to do |format|
|
82
|
+
format.html { render 'cadmus/pages/index' }
|
83
|
+
format.xml { render :xml => @pages }
|
84
|
+
format.json { render :json => @pages }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def show
|
89
|
+
respond_to do |format|
|
90
|
+
format.html { render 'cadmus/pages/show' }
|
91
|
+
format.xml { render :xml => @page }
|
92
|
+
format.json { render :json => @page }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def new
|
97
|
+
@page = page_scope.new(params[:page])
|
98
|
+
|
99
|
+
respond_to do |format|
|
100
|
+
format.html { render 'cadmus/pages/new' }
|
101
|
+
format.xml { render :xml => @page }
|
102
|
+
format.json { render :json => @page }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def edit
|
107
|
+
render 'cadmus/pages/edit'
|
108
|
+
end
|
109
|
+
|
110
|
+
def create
|
111
|
+
@page = page_scope.new(params[:page])
|
112
|
+
|
113
|
+
respond_to do |format|
|
114
|
+
if @page.save
|
115
|
+
dest = { :action => 'show', :page_glob => @page.slug }
|
116
|
+
format.html { redirect_to(dest, :notice => 'Page was successfully created.') }
|
117
|
+
format.xml { render :xml => @page, :status => :created, :location => dest }
|
118
|
+
format.json { render :json => @page, :status => :created, :location => dest }
|
119
|
+
else
|
120
|
+
format.html { render 'cadmus/pages/new' }
|
121
|
+
format.xml { render :xml => @page.errors, :status => :unprocessable_entity }
|
122
|
+
format.json { render :json => @page.errors, :status => :unprocessable_entity }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def update
|
128
|
+
respond_to do |format|
|
129
|
+
if @page.update_attributes(params[:page])
|
130
|
+
dest = { :action => 'show', :page_glob => @page.slug }
|
131
|
+
format.html { redirect_to(dest, :notice => 'Page was successfully updated.') }
|
132
|
+
format.xml { head :ok }
|
133
|
+
format.json { head :ok }
|
134
|
+
else
|
135
|
+
format.html { render 'cadmus/pages/edit' }
|
136
|
+
format.xml { render :xml => @page.errors, :status => :unprocessable_entity }
|
137
|
+
format.json { render :json => @page.errors, :status => :unprocessable_entity }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def destroy
|
143
|
+
@page.destroy
|
144
|
+
|
145
|
+
respond_to do |format|
|
146
|
+
format.html { redirect_to(:action => :index) }
|
147
|
+
format.xml { head :ok }
|
148
|
+
format.json { head :ok }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
# This gets kind of meta.
|
155
|
+
#
|
156
|
+
# If page_parent_name and page_parent_class are both defined for this class, this method uses it to find
|
157
|
+
# the parent object in which pages live. For example, if page_parent_class is Blog and page_parent_name
|
158
|
+
# is "blog", then this is equivalent to calling:
|
159
|
+
#
|
160
|
+
# @page_parent = Blog.where(:id => params["blog_id"]).first
|
161
|
+
#
|
162
|
+
# If you don't want to use :id to find the parent object, then redefine the find_parent_by method to return
|
163
|
+
# what you want to use.
|
164
|
+
def page_parent
|
165
|
+
return @page_parent if @page_parent
|
166
|
+
|
167
|
+
if page_parent_name && page_parent_class
|
168
|
+
parent_id_param = "#{page_parent_name}_id"
|
169
|
+
if params[parent_id_param]
|
170
|
+
@page_parent = page_parent_class.where(find_parent_by => params[parent_id_param]).first
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
@page_parent
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns the name of the page parent object. This will be used for determining the parameter name for
|
178
|
+
# finding the parent object. For example, if the page parent name is "wiki", the finder will look in
|
179
|
+
# params["wiki_id"] to determine the object ID.
|
180
|
+
#
|
181
|
+
# By default, this will return the value of page_parent_name set at the controller class level, but can
|
182
|
+
# be overridden for cases where the page parent name must be determined on a per-request basis.
|
183
|
+
def page_parent_name
|
184
|
+
self.class.page_parent_name
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the class of the page parent object. For example, if the pages used by this controller are
|
188
|
+
# children of a Section object, this method should return the Section class.
|
189
|
+
#
|
190
|
+
# By default, this will return the value of page_parent_class set at the controller class level, but can
|
191
|
+
# be overridden for cases where the page parent class must be determined on a per-request basis.
|
192
|
+
def page_parent_class
|
193
|
+
self.class.page_parent_class
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns the field used to find the page parent object. By default this is :id, but if you need to
|
197
|
+
# find the page parent object using a different parameter (for example, if you use a "slug" field for
|
198
|
+
# part of the URL), this can be changed.
|
199
|
+
#
|
200
|
+
# By default this method takes its value from the "find_parent_by" accessor set at the controller class
|
201
|
+
# level, but it can be overridden for cases where the finder field name should be determined on a
|
202
|
+
# per-request basis.
|
203
|
+
def find_parent_by
|
204
|
+
self.class.find_parent_by || :id
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the ActiveRecord::Relation that will be used for finding pages. If there is a page parent
|
208
|
+
# for this request, this will be the "pages" scope defined by the parent object. If there isn't,
|
209
|
+
# this will be the "global" scope of the page class (i.e. pages with no parent object).
|
210
|
+
def page_scope
|
211
|
+
@page_scope ||= page_parent ? page_parent.pages : page_class.global
|
212
|
+
end
|
213
|
+
|
214
|
+
def load_parent_and_page
|
215
|
+
if params[:page_glob]
|
216
|
+
@page = page_scope.find_by_slug(params[:page_glob])
|
217
|
+
raise ActiveRecord::RecordNotFound.new("No page called #{params[:page_glob]}") unless @page
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'redcarpet'
|
2
|
+
require 'cadmus/renderers'
|
3
|
+
|
4
|
+
module Cadmus
|
5
|
+
module Markdown
|
6
|
+
# A Redcarpet renderer that outputs HTML and uses SmartyPants smart
|
7
|
+
# quotes.
|
8
|
+
class HtmlRenderer < Redcarpet::Render::HTML
|
9
|
+
include Redcarpet::Render::SmartyPants
|
10
|
+
end
|
11
|
+
|
12
|
+
# A Redcarpet renderer that outputs formatted plain text (that looks quite similar to
|
13
|
+
# the Markdown input sent to it).
|
14
|
+
class TextRenderer < Redcarpet::Render::Base
|
15
|
+
def normal_text(text)
|
16
|
+
text
|
17
|
+
end
|
18
|
+
|
19
|
+
def block_code(text, language)
|
20
|
+
normal_text(text)
|
21
|
+
end
|
22
|
+
|
23
|
+
def codespan(text)
|
24
|
+
normal_text(text)
|
25
|
+
end
|
26
|
+
|
27
|
+
def header(title, level)
|
28
|
+
case level
|
29
|
+
when 1
|
30
|
+
"#{title.upcase}\n#{'=' * title.length}\n\n"
|
31
|
+
when 2
|
32
|
+
"#{title}\n#{'-' * title.length}\n\n"
|
33
|
+
when 3
|
34
|
+
"#{title.upcase}\n\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def double_emphasis(text)
|
39
|
+
"**#{text}**"
|
40
|
+
end
|
41
|
+
|
42
|
+
def emphasis(text)
|
43
|
+
"*#{text}*"
|
44
|
+
end
|
45
|
+
|
46
|
+
def linebreak
|
47
|
+
"\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
def paragraph(text)
|
51
|
+
"#{text}\n\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
def list(content, list_type)
|
55
|
+
"#{content}\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
def list_item(content, list_type)
|
59
|
+
" * #{content}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Renderers
|
65
|
+
|
66
|
+
# A Cadmus renderer that handles Markdown input using the Redcarpet rendering engine.
|
67
|
+
# It can produce +:html+ and +:text+ formats, using Cadmus::Markdown::HtmlRenderer
|
68
|
+
# and Cadmus::Markdown::TextRenderer.
|
69
|
+
#
|
70
|
+
# Liquid is rendered first, then the result is processed as Markdown.
|
71
|
+
class Markdown < Base
|
72
|
+
|
73
|
+
# Additional options to be passed as the second argument to the Redcarpet::Markdown
|
74
|
+
# constructor.
|
75
|
+
attr_accessor :markdown_options
|
76
|
+
|
77
|
+
def initialize
|
78
|
+
super
|
79
|
+
@markdown_options = {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def markdown_options=(opts)
|
83
|
+
@markdown_options = opts
|
84
|
+
@redcarpet_instance = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def preprocess(template, format, options={})
|
88
|
+
redcarpet_instance.render(super)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def markdown_renderer(format)
|
93
|
+
case format.to_sym
|
94
|
+
when :html
|
95
|
+
Cadmus::Markdown.HtmlRenderer
|
96
|
+
when :text
|
97
|
+
Cadmus::Markdown.TextRenderer
|
98
|
+
else
|
99
|
+
raise "Format #{format.inspect} is not supported by #{self.class.name}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def redcarpet_instance(format)
|
104
|
+
Redcarpet::Markdown.new(markdown_renderer(format), markdown_options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# An alternative to Cadmus::Renderable that will use Cadmus::Renderers::Markdown as the
|
110
|
+
# renderer class. Additionally, it will set the renderer's +markdown_options+ to the
|
111
|
+
# return value of the +markdown_options+ method, if that method is defined.
|
112
|
+
module MarkdownRenderable
|
113
|
+
include Renderable
|
114
|
+
|
115
|
+
def setup_renderer
|
116
|
+
super
|
117
|
+
renderer.markdown_options = markdown_options if respond_to?(:markdown_options)
|
118
|
+
end
|
119
|
+
|
120
|
+
def cadmus_renderer_class
|
121
|
+
Cadmus::Renderers::Markdown
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/cadmus/page.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'liquid'
|
2
|
+
|
3
|
+
module Cadmus
|
4
|
+
|
5
|
+
# Adds a +cadmus_page+ extension method to ActiveRecord::Base that sets up a class as a page-like object for
|
6
|
+
# Cadmus.
|
7
|
+
module Page
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Sets up a model to behave as a Cadmus page. This will add the following behaviors:
|
13
|
+
#
|
14
|
+
# * A slug and slug generator field using HasSlug
|
15
|
+
# * A name field that determines the name of the page for administrative UI
|
16
|
+
# * An optional, polymorphic +parent+ field
|
17
|
+
# * A scope called +global+ that returns instances of this class that have no parent
|
18
|
+
# * A +liquid_template+ method that parses the value of this model's +content+ field as a Liquid
|
19
|
+
# template
|
20
|
+
# * Validators that ensure that this page has a name, that this page's slug is unique within the
|
21
|
+
# parent object, and that the slug isn't "pages" or "edit" (which are used for admin UI)
|
22
|
+
#
|
23
|
+
# @param options [Hash] options to modify the default behavior
|
24
|
+
# @option options :name_field the name of the field to be used as the page name. Defaults to +:name+.
|
25
|
+
# @option options :slug_field the name of the field to be used as the page slug. Defaults to +:slug+.
|
26
|
+
# @option options :slug_generator_field the name of the field to be used as the slug generator.
|
27
|
+
# Defaults to the value of +name_field+ if unspecified.
|
28
|
+
def cadmus_page(options={})
|
29
|
+
options[:slug_generator_field] = options[:name_field] unless options.has_key?(:slug_generator_field)
|
30
|
+
has_slug(options)
|
31
|
+
|
32
|
+
cattr_accessor :name_field
|
33
|
+
self.name_field = (options.delete(:name_field) || :name).to_s
|
34
|
+
|
35
|
+
belongs_to :parent, :polymorphic => true
|
36
|
+
|
37
|
+
validates_presence_of name_field
|
38
|
+
validates_uniqueness_of slug_field, :scope => [:parent_id, :parent_type]
|
39
|
+
validates_exclusion_of slug_field, :in => %w(pages edit)
|
40
|
+
|
41
|
+
scope :global, :conditions => { :parent_id => nil, :parent_type => nil }
|
42
|
+
|
43
|
+
class_eval do
|
44
|
+
def liquid_template
|
45
|
+
Liquid::Template.parse(content)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ActiveRecord::Base.send :include, Cadmus::Page
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Cadmus
|
2
|
+
|
3
|
+
# A Cadmus renderer is an object that handles the rendering of +Liquid::Template+s to output formats
|
4
|
+
# such as HTML or plain text. A renderer provides several features over and above what plain
|
5
|
+
# Liquid does:
|
6
|
+
#
|
7
|
+
# * Automatic removal of HTML tags for plain text output
|
8
|
+
# * Integration with Rails 3's HTML escaping functionality
|
9
|
+
# * Ability to specify default assigns, filters, and registers and augment them on a per-call basis
|
10
|
+
# * Ability to render to multiple output formats from a single renderer
|
11
|
+
module Renderers
|
12
|
+
|
13
|
+
# The simplest Cadmus renderer. It will render Liquid templates to HTML and plain text (removing the
|
14
|
+
# HTML tags from the plain text output).
|
15
|
+
class Base
|
16
|
+
attr_accessor :default_assigns, :default_filters, :default_registers, :html_sanitizer
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
self.default_registers = {}
|
20
|
+
self.default_filters = []
|
21
|
+
self.default_assigns = {}
|
22
|
+
|
23
|
+
self.html_sanitizer = Rails.application.config.action_view.full_sanitizer || HTML::FullSanitizer.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# The preprocess method performs the initial rendering of the Liquid template using the a combination
|
27
|
+
# of the default_filters, default_assigns, default_registers, and any :assigns, :filters, and :registers
|
28
|
+
# options passed in as options.
|
29
|
+
#
|
30
|
+
# @param [Liquid::Template] template the Liquid template to render.
|
31
|
+
# @param [Symbol] format the format being used for rendering. (This is ignored in the Base implementation
|
32
|
+
# of +preprocess+ and is only used in Base's +render+ method, but is passed here in case subclasses wish to
|
33
|
+
# make use of the format information when overriding preprocess.)
|
34
|
+
# @param [Hash] options additional options that can be passed to override default rendering behavior.
|
35
|
+
# @option options [Hash] :assigns additional assign variables that will be made available to the template.
|
36
|
+
# @option options [Hash] :registers additional register variables that will be made available to the template.
|
37
|
+
# @option options [Hash] :filters additional filters to be made available to the template.
|
38
|
+
# @return [String] the raw results of rendering the Liquid template.
|
39
|
+
def preprocess(template, format, options={})
|
40
|
+
render_args = [
|
41
|
+
default_assigns.merge(options[:assigns] || {}),
|
42
|
+
{
|
43
|
+
:filters => default_filters + (options[:filters] || []),
|
44
|
+
:registers => default_registers.merge(options[:registers] || {})
|
45
|
+
}
|
46
|
+
]
|
47
|
+
|
48
|
+
template.render(*render_args)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Render a given Liquid template to the specified format. This renderer implementation supports +:html+ and
|
52
|
+
# +:text+ formats, but other implementations may support other formats.
|
53
|
+
#
|
54
|
+
# @param [Liquid::Template] template the Liquid template to render.
|
55
|
+
# @param [Symbol] format the format being used for rendering. +:html+ will result in a string that's marked
|
56
|
+
# as safe for HTML rendering (and thus won't be escaped by Rails). +:text+ will strip all HTML tags out
|
57
|
+
# of the template result.
|
58
|
+
# @param [Hash] options additional rendering options. See the +preprocess+ method for a description of
|
59
|
+
# available options.
|
60
|
+
def render(template, format, options={})
|
61
|
+
content = preprocess(template, format, options)
|
62
|
+
|
63
|
+
case format.to_sym
|
64
|
+
when :html
|
65
|
+
content.html_safe
|
66
|
+
when :text
|
67
|
+
html_sanitizer.sanitize content
|
68
|
+
else
|
69
|
+
raise "Format #{format.inspect} unsupported by #{self.class.name}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# A helper module that can be included in classes that wish to provide a Cadmus renderer. For
|
76
|
+
# example, an Email class might want to provide a renderer so that it can be easily transformed
|
77
|
+
# into HTML or text formats.
|
78
|
+
#
|
79
|
+
# This will expose a +cadmus_renderer+ method that constructs a new renderer. Optionally, you
|
80
|
+
# can implement methods called +liquid_assigns+, +liquid_registers+, and +liquid_filters+, which
|
81
|
+
# will specify the default assigns, registers, and filters for the renderer. You can also
|
82
|
+
# override the +cadmus_renderer_class+ method if you want to use a renderer class aside from
|
83
|
+
# Cadmus::Renderers::Base.
|
84
|
+
#
|
85
|
+
# == Example
|
86
|
+
#
|
87
|
+
# class Email < ActiveRecord::Base
|
88
|
+
# include Cadmus::Renderable
|
89
|
+
#
|
90
|
+
# def liquid_template
|
91
|
+
# Liquid::Template.new(self.content)
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# def liquid_assigns
|
95
|
+
# { recipient: self.recipient, sender: self.sender, sent_at: self.sent_at }
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
module Renderable
|
99
|
+
|
100
|
+
# @return a new Cadmus renderer set up using the +default_assigns+, +default_registers+
|
101
|
+
# and +default_filters+ methods, if they exist.
|
102
|
+
def cadmus_renderer
|
103
|
+
cadmus_renderer_class.new.tap { |renderer| setup_renderer(renderer) }
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return the Cadmus renderer class to instanciate in the +cadmus_renderer+ method. By
|
107
|
+
# default, Cadmus::Renderers::Base.
|
108
|
+
def cadmus_renderer_class
|
109
|
+
Cadmus::Renderers::Base
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
# Sets the values of +default_assigns+, +default_registers+ and +default_filters+ on a given
|
114
|
+
# renderer using the +liquid_assigns+, +liquid_registers+ and +liquid_filters+ methods, if
|
115
|
+
# they're defined.
|
116
|
+
def setup_renderer(renderer)
|
117
|
+
renderer.default_assigns = liquid_assigns if respond_to?(:liquid_assigns)
|
118
|
+
renderer.default_registers = liquid_registers if respond_to?(:liquid_registers)
|
119
|
+
renderer.default_filters = liquid_filters if respond_to?(:liquid_filters)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Cadmus
|
2
|
+
# A routing constraint that determines whether a request has a valid Cadmus page glob. A
|
3
|
+
# page glob consists of one or more valid slug parts separated by forward slashes. A valid
|
4
|
+
# slug part consists of a lower-case letter followed by any combination of lower-case letters,
|
5
|
+
# digits, and hyphens.
|
6
|
+
class SlugConstraint
|
7
|
+
# @param request an HTTP request object.
|
8
|
+
# @return [Boolean] true if this request's +:page_glob+ parameter is a valid Cadmus page
|
9
|
+
# glob, false if it's not. Allows +:page_glob+ to be nil only if the Rails environment
|
10
|
+
# is +test+, because +assert_recognizes+ doesn't always pass the full params hash
|
11
|
+
# including globbed parameters.
|
12
|
+
def matches?(request)
|
13
|
+
page_glob = request.symbolized_path_parameters[:page_glob]
|
14
|
+
|
15
|
+
# assert_recognizes doesn't pass the full params hash as we would in a real Rails
|
16
|
+
# application. So we have to always pass this constraint if we're testing.
|
17
|
+
return true if page_glob.nil? && Rails.env.test?
|
18
|
+
|
19
|
+
page_glob.sub(/^\//, '').split(/\//).all? do |part|
|
20
|
+
part =~ /^[a-z][a-z0-9\-]*$/
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
ActionDispatch::Routing::Mapper.class_eval do
|
27
|
+
# Defines a "cadmus_pages" DSL command you can use in config/routes.rb. This sets up a Cadmus
|
28
|
+
# PagesController that will accept the following routes:
|
29
|
+
#
|
30
|
+
# * GET /pages -> PagesController#index
|
31
|
+
# * GET /pages/new -> PagesController#new
|
32
|
+
# * POST /pages -> PagesController#create
|
33
|
+
# * GET /pages/anything/possibly-including/slashes/edit -> PagesController#edit
|
34
|
+
# * GET /pages/anything/possibly-including/slashes -> PagesController#show
|
35
|
+
# * PUT /pages/anything/possibly-including/slashes -> PagesController#update
|
36
|
+
# * DELETE /pages/anything/possibly-including/slashes -> PagesController#destroy
|
37
|
+
#
|
38
|
+
# cadmus_pages accepts two additional options:
|
39
|
+
#
|
40
|
+
# * :controller - changes which controller it maps to. By default, it is "pages" (meaning PagesController).
|
41
|
+
# * :shallow - if set to "true", the edit, show, update and destroy routes won't include the "/pages" prefix. Useful if you're
|
42
|
+
# already inside a unique prefix.
|
43
|
+
def cadmus_pages(options)
|
44
|
+
options = options.with_indifferent_access
|
45
|
+
|
46
|
+
controller = options[:controller] || 'pages'
|
47
|
+
|
48
|
+
get "pages" => "#{controller}#index", :as => 'pages'
|
49
|
+
get "pages/new" => "#{controller}#new", :as => 'new_page'
|
50
|
+
post "pages" => "#{controller}#create"
|
51
|
+
|
52
|
+
slug_constraint = Cadmus::SlugConstraint.new
|
53
|
+
|
54
|
+
page_actions = Proc.new do
|
55
|
+
get "*page_glob/edit" => "#{controller}#edit", :as => 'edit_page', :constraints => slug_constraint
|
56
|
+
get "*page_glob" => "#{controller}#show", :as => 'page', :constraints => slug_constraint
|
57
|
+
put "*page_glob" => "#{controller}#update", :constraints => slug_constraint
|
58
|
+
delete "*page_glob" => "#{controller}#destroy", :constraints => slug_constraint
|
59
|
+
end
|
60
|
+
|
61
|
+
if options[:shallow]
|
62
|
+
instance_eval(&page_actions)
|
63
|
+
else
|
64
|
+
scope 'pages' do
|
65
|
+
instance_eval(&page_actions)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/cadmus/slugs.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
module Cadmus
|
2
|
+
module Slugs
|
3
|
+
|
4
|
+
# Tests whether a string is a valid Cadmus slug or not. A valid Cadmus slug
|
5
|
+
# consists of one or more valid slug parts separated by forward slashes. A valid
|
6
|
+
# slug part consists of a lower-case letter followed by any combination of lower-case letters,
|
7
|
+
# digits, and hyphens.
|
8
|
+
#
|
9
|
+
# For example, +about-us/people+, +special-deals+, and +winter-2012+ are all valid slugs, but
|
10
|
+
# +3-things+, +123+, +nobody-lives-here!+, and +/root-page+ aren't.
|
11
|
+
SLUG_REGEX = /^([a-z][a-z0-9\-]*\/)*[a-z][a-z0-9\-]*$/
|
12
|
+
|
13
|
+
# Converts a string to a valid slug part by changing all whitespace to hyphens, converting all
|
14
|
+
# upper-case letters to lower-case, removing all remaining non-alphanumeric, non-hyphen
|
15
|
+
# characters, and removing any non-alphabetical characters at the beginning of the string.
|
16
|
+
#
|
17
|
+
# For example:
|
18
|
+
# * "Katniss Everdeen" becomes "katniss-everdeen"
|
19
|
+
# * "21 guns" becomes "guns"
|
20
|
+
# * "We love you, Conrad!!!1" becomes "we-love-you-conrad1"
|
21
|
+
def self.slugify(string)
|
22
|
+
string.to_s.downcase.gsub(/\s+/, '-').gsub(/[^a-z0-9\-]/, '').sub(/^[^a-z]+/, '')
|
23
|
+
end
|
24
|
+
|
25
|
+
# An extension for ActiveRecord::Base that adds a +has_slug+ method. This can also be
|
26
|
+
# safely included in non-ActiveRecord objects to allow for a similar method, but those
|
27
|
+
# objects have to at least include ActiveModel::Validations.
|
28
|
+
module HasSlug
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
|
33
|
+
# Sets up an automatic slug-generating field on this class. There is a slug field,
|
34
|
+
# which will store the resulting slug, and a slug generator field, which, if set,
|
35
|
+
# will automatically generate a slugified version of its content and store it in
|
36
|
+
# the slug field.
|
37
|
+
#
|
38
|
+
# Additionally, +has_slug+ sets up a format validator for the slug field to ensure
|
39
|
+
# that it's a valid Cadmus slug, and defines +to_param+ to return the slug (so
|
40
|
+
# that links to the slugged object can use the slug in their URL).
|
41
|
+
#
|
42
|
+
# +has_slug+ attempts to be smart about detecting when the user has manually set a
|
43
|
+
# slug for the object and not overwriting it. Auto-generated slugs are only used
|
44
|
+
# when there is not already a slug set.
|
45
|
+
#
|
46
|
+
# @param [Hash] options options to override the default behavior.
|
47
|
+
# @option options slug_field the name of the slug field. Defaults to +:slug+.
|
48
|
+
# @option options slug_generator_field the name of the slug generator field. Defaults
|
49
|
+
# to +:name+.
|
50
|
+
def has_slug(options={})
|
51
|
+
cattr_accessor :slug_field, :slug_generator_field
|
52
|
+
|
53
|
+
self.slug_field = (options.delete(:slug_field) || :slug).to_s
|
54
|
+
self.slug_generator_field = (options.delete(:slug_generator_field) || :name).to_s
|
55
|
+
|
56
|
+
validates_format_of slug_field, :with => Cadmus::Slugs::SLUG_REGEX
|
57
|
+
|
58
|
+
class_eval <<-EOF
|
59
|
+
def #{slug_generator_field}=(new_value)
|
60
|
+
write_attribute(:#{slug_generator_field}, new_value)
|
61
|
+
if #{slug_field}.blank?
|
62
|
+
self.#{slug_field} = Cadmus::Slugs.slugify(new_value)
|
63
|
+
@auto_assigned_slug = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# If the user enters a title and no slug, don't overwrite the auto-assigned one
|
68
|
+
def #{slug_field}=(new_slug)
|
69
|
+
return if new_slug.blank? && @auto_assigned_slug
|
70
|
+
write_attribute(:#{slug_field}, new_slug)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_param
|
74
|
+
#{slug_field}
|
75
|
+
end
|
76
|
+
EOF
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
ActiveRecord::Base.send :include, Cadmus::Slugs::HasSlug
|
data/lib/cadmus.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cadmus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nat Budin
|
9
|
+
- Aziz Khoury
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-04-18 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
requirement: &70361178912560 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70361178912560
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: liquid
|
28
|
+
requirement: &70361178912140 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70361178912140
|
37
|
+
description: Why deal with setting up a separate CMS? Cadmus is just a little bit
|
38
|
+
of CMS and fits nicely into your existing app. It can be used for allowing users
|
39
|
+
to customize areas of the site, for creating editable "about us" pages, and more.
|
40
|
+
email:
|
41
|
+
- natbudin@gmail.com
|
42
|
+
- bentael@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- .gitignore
|
48
|
+
- .yardopts
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- app/assets/javascripts/cadmus.previewablehtml.js
|
54
|
+
- app/views/cadmus/pages/_form.html.erb
|
55
|
+
- app/views/cadmus/pages/edit.html.erb
|
56
|
+
- app/views/cadmus/pages/index.html.erb
|
57
|
+
- app/views/cadmus/pages/new.html.erb
|
58
|
+
- app/views/cadmus/pages/show.html.erb
|
59
|
+
- cadmus.gemspec
|
60
|
+
- lib/cadmus.rb
|
61
|
+
- lib/cadmus/controller_extensions.rb
|
62
|
+
- lib/cadmus/markdown.rb
|
63
|
+
- lib/cadmus/page.rb
|
64
|
+
- lib/cadmus/renderers.rb
|
65
|
+
- lib/cadmus/routing.rb
|
66
|
+
- lib/cadmus/slugs.rb
|
67
|
+
- lib/cadmus/version.rb
|
68
|
+
homepage: http://github.com/gively/cadmus
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.8.11
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Embeddable CMS for Rails 3 apps
|
92
|
+
test_files: []
|
93
|
+
has_rdoc:
|