cadmus 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --protected
2
+ --markup-provider=redcarpet
3
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cadmus.gemspec
4
+ gemspec
5
+
6
+ gem 'yard'
7
+ gem 'redcarpet', '~> 1.0'
8
+ gem 'github-markup'
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 &copy; 2011-2012 Gively, Inc. Cadmus is released under the MIT license. For more information, see the LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,7 @@
1
+ <h1>New page</h1>
2
+
3
+ <%= form_for(@page, :url => { :action => :create }, :class => "new_cadmus_page") do |f| %>
4
+ <%= render :partial => 'cadmus/pages/form', :locals => { :f => f } %>
5
+ <% end -%>
6
+
7
+ <%= link_to 'Back', { :action => 'index' } %>
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Cadmus
2
+ VERSION = "0.4.1"
3
+ end
data/lib/cadmus.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "cadmus/version"
2
+ require "cadmus/routing"
3
+ require "cadmus/renderers"
4
+ require "cadmus/controller_extensions"
5
+ require "cadmus/slugs"
6
+ require "cadmus/page"
7
+ require "rails"
8
+
9
+ module Cadmus
10
+ class Engine < Rails::Engine
11
+ end
12
+ end
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: