k3cms_blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +136 -0
- data/License.txt +165 -0
- data/Rakefile +20 -0
- data/Readme.rdoc +13 -0
- data/app/cells/k3cms/blog/blog_posts/index.html.haml +46 -0
- data/app/cells/k3cms/blog/blog_posts/index.html.haml.haml +0 -0
- data/app/cells/k3cms/blog/blog_posts/metadata_drawer.html.haml +38 -0
- data/app/cells/k3cms/blog/blog_posts/published_status.html.haml +4 -0
- data/app/cells/k3cms/blog/blog_posts_cell.rb +36 -0
- data/app/controllers/k3cms/blog/base_controller.rb +29 -0
- data/app/controllers/k3cms/blog/blog_posts_controller.rb +82 -0
- data/app/models/k3cms/blog/ability.rb +37 -0
- data/app/models/k3cms/blog/blog_post.rb +75 -0
- data/app/models/user_decorator.rb +3 -0
- data/app/views/k3cms/blog/blog_posts/_form.html.erb +28 -0
- data/app/views/k3cms/blog/blog_posts/edit.html.erb +3 -0
- data/app/views/k3cms/blog/blog_posts/index.html.haml +3 -0
- data/app/views/k3cms/blog/blog_posts/new.html.erb +5 -0
- data/app/views/k3cms/blog/blog_posts/show.html.haml +41 -0
- data/app/views/k3cms/blog/init.html.haml +34 -0
- data/config/authorization.rb +37 -0
- data/config/locales/validates_timeliness.en.yml +16 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20110113015852_create_k3_blog_posts.rb +17 -0
- data/db/migrate/20110118015034_create_slugs.rb +18 -0
- data/db/migrate/20110118023300_add_cached_slug_to_blog_posts.rb +10 -0
- data/db/migrate/20110120172202_add_meta_description_to_blog_posts.rb +15 -0
- data/db/migrate/20110415180204_rename_to_k3cms_blog_posts.rb +9 -0
- data/image_source/famfamfam_silk_icons/information.png +0 -0
- data/image_source/famfamfam_silk_icons/page_add.png +0 -0
- data/image_source/famfamfam_silk_icons/page_delete.png +0 -0
- data/image_source/famfamfam_silk_icons/page_white_add.png +0 -0
- data/image_source/famfamfam_silk_icons/text_list_bullets.png +0 -0
- data/image_source/icons.xcf +0 -0
- data/k3cms_blog.gemspec +37 -0
- data/lib/form_tag_helper.rb +3 -0
- data/lib/k3cms/blog/railtie.rb +67 -0
- data/lib/k3cms/blog/version.rb +5 -0
- data/lib/k3cms_blog.rb +8 -0
- data/lib/tasks/tasks.rake +17 -0
- data/public/images/k3cms/blog/icons.png +0 -0
- data/public/images/k3cms/blog/new.png +0 -0
- data/public/javascripts/k3cms/blog.js +13 -0
- data/public/stylesheets/k3cms/blog.css +53 -0
- data/spec/connection_and_schema.rb +12 -0
- data/spec/models/blog_post_spec.rb +254 -0
- data/spec/spec_helper.rb +49 -0
- metadata +340 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module K3cms
|
2
|
+
module Blog
|
3
|
+
class BaseController < ApplicationController
|
4
|
+
|
5
|
+
include CanCan::ControllerAdditions
|
6
|
+
helper K3cms::InlineEditor::InlineEditorHelper
|
7
|
+
|
8
|
+
def current_ability
|
9
|
+
@current_ability ||= K3cms::Blog::Ability.new(k3cms_user)
|
10
|
+
end
|
11
|
+
|
12
|
+
rescue_from CanCan::AccessDenied do |exception|
|
13
|
+
k3cms_authorization_required(exception)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
# Unfortunately, FriendlyId raises an error for some things instead of just adding a validation error to the errors array.
|
18
|
+
# Let's try to respond the same as we would for a validation error though.
|
19
|
+
rescue_from ::FriendlyId::BlankError, :with => :rescue_friendly_id_blank_error
|
20
|
+
|
21
|
+
def rescue_friendly_id_blank_error
|
22
|
+
respond_to do |format|
|
23
|
+
format.json { render :json => {:error => 'Cannot be blank'} }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module K3cms
|
2
|
+
module Blog
|
3
|
+
class BlogPostsController < K3cms::Blog::BaseController
|
4
|
+
load_and_authorize_resource :blog_post, :class => 'K3cms::Blog::BlogPost'
|
5
|
+
|
6
|
+
def index
|
7
|
+
@blog_posts = @blog_posts.order('id desc')
|
8
|
+
respond_to do |format|
|
9
|
+
format.html # index.html.erb
|
10
|
+
format.xml { render :xml => @blog_posts }
|
11
|
+
format.json { render :json => @blog_posts }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def show
|
16
|
+
respond_to do |format|
|
17
|
+
format.html # show.html.erb
|
18
|
+
format.xml { render :xml => @blog_post }
|
19
|
+
format.json { render :json => @blog_post }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def new
|
24
|
+
respond_to do |format|
|
25
|
+
format.html # new.html.erb
|
26
|
+
format.xml { render :xml => @blog_post }
|
27
|
+
format.json { render :json => @blog_post }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def create
|
32
|
+
@blog_post.attributes = params[:k3cms_blog_blog_post]
|
33
|
+
@blog_post.author = current_user
|
34
|
+
|
35
|
+
respond_to do |format|
|
36
|
+
if @blog_post.save
|
37
|
+
format.html do
|
38
|
+
#redirect_to(k3cms_blog_blog_post_url(@blog_post),
|
39
|
+
|
40
|
+
redirect_to(k3cms_blog_blog_posts_url(:focus => "##{dom_id(@blog_post)} .editable[data-attribute=title]"),
|
41
|
+
:notice => 'Blog post was successfully created.')
|
42
|
+
end
|
43
|
+
format.xml { render :xml => @blog_post, :status => :created, :location => @blog_post }
|
44
|
+
format.json { render :nothing => true }
|
45
|
+
else
|
46
|
+
format.html { render :action => "new" }
|
47
|
+
format.xml { render :xml => @blog_post.errors, :status => :unprocessable_entity }
|
48
|
+
format.json { render :nothing => true }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def edit
|
54
|
+
end
|
55
|
+
|
56
|
+
def update
|
57
|
+
respond_to do |format|
|
58
|
+
if @blog_post.update_attributes(params[:k3cms_blog_blog_post])
|
59
|
+
format.html { redirect_to(k3cms_blog_blog_post_url(@blog_post), :notice => 'Blog post was successfully updated.') }
|
60
|
+
format.xml { head :ok }
|
61
|
+
format.json { render :json => {} }
|
62
|
+
else
|
63
|
+
format.html { render :action => "edit" }
|
64
|
+
format.xml { render :xml => @blog_post.errors, :status => :unprocessable_entity }
|
65
|
+
format.json { render :json => {:error => @blog_post.errors.full_messages.join('<br/>')} }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def destroy
|
71
|
+
@blog_post.destroy
|
72
|
+
respond_to do |format|
|
73
|
+
#format.html { redirect_to(k3cms_blog_blog_posts_url) }
|
74
|
+
format.html { redirect_to k3cms_blog_blog_posts_url }
|
75
|
+
format.xml { head :ok }
|
76
|
+
format.json { render :nothing => true }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module K3cms
|
2
|
+
module Blog
|
3
|
+
class Ability
|
4
|
+
include CanCan::Ability
|
5
|
+
|
6
|
+
def initialize(user)
|
7
|
+
if user.k3cms_permitted?(:view_blog_post)
|
8
|
+
can :read, K3cms::Blog::BlogPost, [] do |blog_post|
|
9
|
+
blog_post.published?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
if user.k3cms_permitted?(:edit_blog_post)
|
14
|
+
can :read, K3cms::Blog::BlogPost
|
15
|
+
can :update, K3cms::Blog::BlogPost
|
16
|
+
end
|
17
|
+
|
18
|
+
if user.k3cms_permitted?(:edit_own_blog_post)
|
19
|
+
can :read, K3cms::Blog::BlogPost
|
20
|
+
can :update, K3cms::Blog::BlogPost, :author_id => user.id
|
21
|
+
end
|
22
|
+
|
23
|
+
if user.k3cms_permitted?(:create_blog_post)
|
24
|
+
can :create, K3cms::Blog::BlogPost
|
25
|
+
end
|
26
|
+
|
27
|
+
if user.k3cms_permitted?(:delete_blog_post)
|
28
|
+
can :destroy, K3cms::Blog::BlogPost
|
29
|
+
end
|
30
|
+
|
31
|
+
if user.k3cms_permitted?(:delete_own_blog_post)
|
32
|
+
can :destroy, K3cms::Blog::BlogPost, :author_id => user.id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
module K3cms
|
3
|
+
module Blog
|
4
|
+
class BlogPost < ActiveRecord::Base
|
5
|
+
self.include_root_in_json = false
|
6
|
+
set_table_name 'k3cms_blog_blog_posts'
|
7
|
+
|
8
|
+
# There are multiple fields/methods involved for the slug/friendly_id. Don't be confused:
|
9
|
+
# * The cached_slug attribute is used when we do BlogPost.find('my-slug')
|
10
|
+
# * The url attribute is never (!!!) "used", except as input to friendly_id, which will then process it and *convert* it to a valid url, as necessary. The result of that, of course, is available in cached_slug.
|
11
|
+
# * The url= setter is left intact, but the url getter is overridden to return cached_slug, because we essentially never want to accidentally use a user-supplied value (read_attribute(:url)) instead of the processed value.
|
12
|
+
# The url attribute will remain nil unless it is ever manually set by the user.
|
13
|
+
# As long as custom_url? is false (the url attribute is nil), it will automatically create a slug based on title any time the title is updated.
|
14
|
+
# As soon as you set the url manually, however, it will stop doing that.
|
15
|
+
has_friendly_id :title_or_custom_url, :use_slug => true
|
16
|
+
|
17
|
+
belongs_to :author, :class_name => 'User'
|
18
|
+
|
19
|
+
normalize_attributes :title, :summary, :body, :with => [:strip, :blank]
|
20
|
+
|
21
|
+
validates :title, :presence => true
|
22
|
+
validates :date, :timeliness => {:type => :date}
|
23
|
+
#validates :url, :uniqueness => true, :allow_nil => true, :allow_blank => true
|
24
|
+
|
25
|
+
after_initialize :set_defaults
|
26
|
+
def set_defaults
|
27
|
+
# Copy summary to body
|
28
|
+
default_summary = '<p>Summary description goes here</p>'
|
29
|
+
self.body = self.attributes['summary'] if self.attributes['body'].nil? && self.attributes['summary'].present? && self.attributes['summary'] != default_summary
|
30
|
+
|
31
|
+
if new_record?
|
32
|
+
self.title = 'New Post' if self.attributes['title'].nil?
|
33
|
+
self.summary = default_summary if self.attributes['summary'].nil?
|
34
|
+
self.date = Date.tomorrow if self.attributes['date'].nil?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
title
|
40
|
+
end
|
41
|
+
|
42
|
+
#---------------------------------------------------------------------------------------------
|
43
|
+
# url/slug/friendly_id
|
44
|
+
private
|
45
|
+
def title_or_custom_url
|
46
|
+
custom_url? ? read_attribute(:url) : title
|
47
|
+
end
|
48
|
+
public
|
49
|
+
def normalize_friendly_id(text)
|
50
|
+
# This uses stringex
|
51
|
+
text.to_url
|
52
|
+
end
|
53
|
+
def url
|
54
|
+
cached_slug
|
55
|
+
end
|
56
|
+
def url=(new)
|
57
|
+
if new
|
58
|
+
new = normalize_friendly_id(new)
|
59
|
+
end
|
60
|
+
# By checking if new != url, we solve the problem where a user might tab from the title field to the url field, and then when they tab out of the url field, it will try to save the current url as a custom url instead of realizing that this is still an automatic url from the title.
|
61
|
+
if new != url # NOT read_attribute(:url) -- we want to compare to the cached_slug based on the title if that's what is currently being used for the slug
|
62
|
+
write_attribute(:url, new)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
def custom_url?
|
66
|
+
read_attribute(:url).present?
|
67
|
+
end
|
68
|
+
|
69
|
+
#---------------------------------------------------------------------------------------------
|
70
|
+
def published?
|
71
|
+
date and Time.zone.now >= date.beginning_of_day
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<%= form_for(@blog_post) do |f| %>
|
2
|
+
<% if @blog_post.errors.any? %>
|
3
|
+
<div id="errorExplanation">
|
4
|
+
<h2><%= pluralize(@blog_post.errors.count, "error") %> prevented this blog post from being saved:</h2>
|
5
|
+
<ul>
|
6
|
+
<% @blog_post.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 :title %><br />
|
15
|
+
<%= f.text_field :title %>
|
16
|
+
</div>
|
17
|
+
<div class="field">
|
18
|
+
<%= f.label :body %><br />
|
19
|
+
<%= f.text_field :body %>
|
20
|
+
</div>
|
21
|
+
<div class="field">
|
22
|
+
<%= f.label :summary %><br />
|
23
|
+
<%= f.text_field :summary %>
|
24
|
+
</div>
|
25
|
+
<div class="actions">
|
26
|
+
<%= f.submit %>
|
27
|
+
</div>
|
28
|
+
<% end %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
.k3cms_blog_blog_posts_show
|
2
|
+
- content_for :title do
|
3
|
+
= strip_tags @blog_post.title
|
4
|
+
|
5
|
+
- if edit_mode? and can?(:edit, K3cms::Blog::BlogPost)
|
6
|
+
:javascript
|
7
|
+
$(function() {
|
8
|
+
$('#k3cms_ribbon').k3cms_ribbon('enable', '.k3cms_blog .blog_post_metadata.button');
|
9
|
+
})
|
10
|
+
|
11
|
+
- k3cms_ribbon_add_drawer :k3cms_blog_blog_post_metadata do
|
12
|
+
= render_cell 'k3cms/blog/blog_posts', :metadata_drawer, :blog_post => @blog_post
|
13
|
+
|
14
|
+
%div{:class => dom_class(@blog_post), :id => dom_id(@blog_post)}
|
15
|
+
= render_cell 'k3cms/blog/blog_posts', :published_status, :blog_post => @blog_post
|
16
|
+
%h2.title
|
17
|
+
- if edit_mode? and can?(:edit, K3cms::Blog::BlogPost)
|
18
|
+
= inline_editable('span', @blog_post, :title) do
|
19
|
+
= raw @blog_post.title
|
20
|
+
- else
|
21
|
+
= raw @blog_post.title
|
22
|
+
|
23
|
+
.date
|
24
|
+
- if edit_mode? and can?(:edit, K3cms::Blog::BlogPost)
|
25
|
+
= inline_editable('span', @blog_post, :date) do
|
26
|
+
= raw @blog_post.date
|
27
|
+
- else
|
28
|
+
= @blog_post.date && @blog_post.date.to_s(:long)
|
29
|
+
|
30
|
+
.body
|
31
|
+
- if edit_mode? and can?(:edit, K3cms::Blog::BlogPost)
|
32
|
+
= inline_editable('div', @blog_post, :body) do
|
33
|
+
= raw @blog_post.body
|
34
|
+
- else
|
35
|
+
= raw @blog_post.body
|
36
|
+
|
37
|
+
.post_bottom
|
38
|
+
-# Comments, etc. go here
|
39
|
+
|
40
|
+
:javascript
|
41
|
+
#{inline_editor_update_page_from_object(@blog_post)}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
- if edit_mode?
|
2
|
+
:javascript
|
3
|
+
$(function() {
|
4
|
+
$('#k3cms_ribbon').k3cms_ribbon({tabs: [
|
5
|
+
new K3cms_Ribbon.Tab('k3cms_blog', {label: 'Blog Post', sections: [
|
6
|
+
new K3cms_Ribbon.Section('k3cms_blog', {items: [
|
7
|
+
new K3cms_Ribbon.Button({
|
8
|
+
element: $('<li/>', { 'class': "icon button blog_post_metadata" }).
|
9
|
+
append($('<a/>', {title: "Post Information", href: "javascript:;", html: ' '})),
|
10
|
+
onClick: function() { $(this).k3cms_ribbon('isEnabled') && $(this).trigger('invoke'); },
|
11
|
+
onInvoke: function() { $('.k3cms_blog_blog_post_metadata.drawer').slideToggle(); $('.k3cms_blog_blog_post_metadata.drawer .editable:visible:eq(0)').focus() }
|
12
|
+
}),
|
13
|
+
new K3cms_Ribbon.Button({
|
14
|
+
element: $('<li/>', { 'class': "icon button list_blog_posts" }).
|
15
|
+
append($('<a/>', {title: "List Blog Posts", href: "#{k3cms_blog_blog_posts_path}", html: ' '})),
|
16
|
+
onMousedown: function(event) {
|
17
|
+
var a = $(this).find('a').eq(0);
|
18
|
+
if ($(this).k3cms_ribbon('isEnabled')) {
|
19
|
+
a.attr('href', a.data('href'));
|
20
|
+
} else {
|
21
|
+
a.data('href', a.attr('href'));
|
22
|
+
a.attr('href', 'javascript:;');
|
23
|
+
}
|
24
|
+
},
|
25
|
+
}),
|
26
|
+
]})
|
27
|
+
]})
|
28
|
+
]})
|
29
|
+
$('#k3cms_ribbon').k3cms_ribbon({
|
30
|
+
always_enabled: [
|
31
|
+
'.k3cms_blog .list_blog_posts.button',
|
32
|
+
]
|
33
|
+
});
|
34
|
+
})
|
@@ -0,0 +1,37 @@
|
|
1
|
+
K3cms::Blog::Railtie.authorization.draw do
|
2
|
+
# First define and describe some suggested permission sets.
|
3
|
+
suggested_permission_set :default, 'Allows managers to create & edit all blog_posts, and delete their own blog_posts'
|
4
|
+
suggested_permission_set :user_creation, 'Allows users to create and manage their own blog_posts'
|
5
|
+
|
6
|
+
# Context makes all abilities defined within to be prefixed with the
|
7
|
+
# singularized version of the given string. Contexts can be nested.
|
8
|
+
context :blog_posts do
|
9
|
+
ability :view, 'Can view a blog_post' # Creates :view_blog_post ability
|
10
|
+
ability :edit, 'Can edit a blog_post'
|
11
|
+
ability :edit_own, 'Can edit only my blog_posts'
|
12
|
+
ability :create, 'Can create a new blog_post'
|
13
|
+
ability :delete, 'Can delete a blog_post'
|
14
|
+
ability :delete_own, 'Can delete only blog_posts created by me'
|
15
|
+
|
16
|
+
# This defines the abilities for the default suggested permission set
|
17
|
+
# in terms of the four default roles (guest, user, manager, admin)
|
18
|
+
extend_suggested_permission_set :default do
|
19
|
+
guest :has => :view
|
20
|
+
# NOTE: :includes_role without first defining the role to be included
|
21
|
+
# will cause an error. The included abilities are limited to
|
22
|
+
# this extend_suggested_permission_set block.
|
23
|
+
user :includes_role => :guest
|
24
|
+
manager :has => [:create, :edit_own, :delete_own], :includes_role => :user
|
25
|
+
# :all only applies to the abilities in this context (:blog_posts)
|
26
|
+
admin :has => :all
|
27
|
+
end
|
28
|
+
|
29
|
+
# Define abilities for the suggested permission set.
|
30
|
+
extend_suggested_permission_set :user_creation do
|
31
|
+
guest :has => :view
|
32
|
+
user :has => [:create, :edit_own, :delete_own], :includes_role => :guest
|
33
|
+
manager :has => :all
|
34
|
+
admin :has => :all
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
en:
|
2
|
+
errors:
|
3
|
+
messages:
|
4
|
+
invalid_date: "is not a valid date"
|
5
|
+
invalid_time: "is not a valid time"
|
6
|
+
invalid_datetime: "is not a valid datetime"
|
7
|
+
is_at: "must be at %{restriction}"
|
8
|
+
before: "must be before %{restriction}"
|
9
|
+
on_or_before: "must be on or before %{restriction}"
|
10
|
+
after: "must be after %{restriction}"
|
11
|
+
on_or_after: "must be on or after %{restriction}"
|
12
|
+
validates_timeliness:
|
13
|
+
error_value_formats:
|
14
|
+
date: '%Y-%m-%d'
|
15
|
+
time: '%H:%M:%S'
|
16
|
+
datetime: '%Y-%m-%d %H:%M:%S'
|
data/config/routes.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateK3BlogPosts < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table 'k3_blog_blog_posts' do |t|
|
4
|
+
t.string 'title'
|
5
|
+
t.string 'url'
|
6
|
+
t.text 'summary'
|
7
|
+
t.text 'body'
|
8
|
+
t.date 'date'
|
9
|
+
t.integer 'author_id'
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table 'k3_blog_blog_posts'
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateSlugs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :slugs do |t|
|
4
|
+
t.string :name
|
5
|
+
t.integer :sluggable_id
|
6
|
+
t.integer :sequence, :null => false, :default => 1
|
7
|
+
t.string :sluggable_type, :limit => 40
|
8
|
+
t.string :scope
|
9
|
+
t.datetime :created_at
|
10
|
+
end
|
11
|
+
add_index :slugs, :sluggable_id
|
12
|
+
add_index :slugs, [:name, :sluggable_type, :sequence, :scope], :name => "index_slugs_on_n_s_s_and_s", :unique => true
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :slugs
|
17
|
+
end
|
18
|
+
end
|