schnitzelpress 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.travis.yml +9 -0
- data/.watchr +15 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +5 -0
- data/Rakefile +7 -0
- data/bin/schnitzelpress +5 -0
- data/lib/public/.gitkeep +0 -0
- data/lib/public/favicon.ico +0 -0
- data/lib/schnitzelpress.rb +30 -0
- data/lib/schnitzelpress/actions/admin.rb +49 -0
- data/lib/schnitzelpress/actions/auth.rb +29 -0
- data/lib/schnitzelpress/actions/blog.rb +70 -0
- data/lib/schnitzelpress/app.rb +45 -0
- data/lib/schnitzelpress/cli.rb +28 -0
- data/lib/schnitzelpress/helpers.rb +72 -0
- data/lib/schnitzelpress/post.rb +168 -0
- data/lib/schnitzelpress/rake.rb +23 -0
- data/lib/schnitzelpress/static.rb +17 -0
- data/lib/schnitzelpress/version.rb +3 -0
- data/lib/templates/new_blog/.gitignore +5 -0
- data/lib/templates/new_blog/Gemfile +19 -0
- data/lib/templates/new_blog/Rakefile +2 -0
- data/lib/templates/new_blog/app.rb.tt +28 -0
- data/lib/templates/new_blog/config.ru.tt +2 -0
- data/lib/templates/new_blog/public/.gitkeep +0 -0
- data/lib/views/404.haml +4 -0
- data/lib/views/admin/admin.haml +12 -0
- data/lib/views/admin/edit.haml +3 -0
- data/lib/views/admin/new.haml +3 -0
- data/lib/views/atom.haml +23 -0
- data/lib/views/blog.scss +1 -0
- data/lib/views/index.haml +6 -0
- data/lib/views/layout.haml +25 -0
- data/lib/views/partials/_admin_post_list.haml +11 -0
- data/lib/views/partials/_disqus.haml +17 -0
- data/lib/views/partials/_form_field.haml +29 -0
- data/lib/views/partials/_gauges.haml +12 -0
- data/lib/views/partials/_google_analytics.haml +10 -0
- data/lib/views/partials/_post.haml +44 -0
- data/lib/views/partials/_post_form.haml +18 -0
- data/lib/views/post.haml +7 -0
- data/lib/views/schnitzelpress.scss +106 -0
- data/schnitzelpress.gemspec +60 -0
- data/spec/app_spec.rb +56 -0
- data/spec/factories.rb +28 -0
- data/spec/post_spec.rb +78 -0
- data/spec/spec_helper.rb +36 -0
- metadata +426 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
:javascript
|
2
|
+
var _gaq = _gaq || [];
|
3
|
+
_gaq.push(['_setAccount', '#{settings.google_analytics_id}']);
|
4
|
+
_gaq.push(['_trackPageview']);
|
5
|
+
|
6
|
+
(function() {
|
7
|
+
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
8
|
+
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
9
|
+
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
10
|
+
})();
|
@@ -0,0 +1,44 @@
|
|
1
|
+
- complete ||= false
|
2
|
+
- show_title ||= post.article_post? && post.title.present?
|
3
|
+
- show_link ||= post.link_post?
|
4
|
+
- show_summary ||= post.summary.present?
|
5
|
+
- show_body ||= complete || !show_summary
|
6
|
+
- show_read_more ||= !complete && post.summary.present?
|
7
|
+
- show_permalink ||= admin_logged_in? || (post.post? && !show_read_more)
|
8
|
+
- show_twitter ||= complete && post.post? && settings.twitter_id.present?
|
9
|
+
|
10
|
+
%article.post{class: [post.status, post.link_post? ? 'link' : 'article']}
|
11
|
+
%header
|
12
|
+
- if show_title
|
13
|
+
%h1
|
14
|
+
%a{href: post.to_url}= post.title
|
15
|
+
- if show_link
|
16
|
+
%h1
|
17
|
+
%a{href: post.link}= post.title
|
18
|
+
➝
|
19
|
+
|
20
|
+
- if show_summary
|
21
|
+
.summary
|
22
|
+
~ markdown post.summary
|
23
|
+
- if show_read_more
|
24
|
+
%p
|
25
|
+
%a{href: url_for(post)}= post.read_more.presence || settings.read_more
|
26
|
+
→
|
27
|
+
|
28
|
+
- if show_body
|
29
|
+
~ post.to_html
|
30
|
+
|
31
|
+
%footer
|
32
|
+
- if show_permalink
|
33
|
+
%p.permalink
|
34
|
+
%a{href: url_for(post)}= post.published_at.try(:to_date) || "∞"
|
35
|
+
- if admin_logged_in?
|
36
|
+
·
|
37
|
+
%a{href: "/admin/edit/#{post.id}"} edit
|
38
|
+
|
39
|
+
- if show_twitter
|
40
|
+
.social_media_buttons
|
41
|
+
- if show_twitter
|
42
|
+
%a{ href: "https://twitter.com/share", class: "twitter-share-button", data: { via: settings.twitter_id } }
|
43
|
+
:javascript
|
44
|
+
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
|
@@ -0,0 +1,18 @@
|
|
1
|
+
%form.post{ action: @post.new_record? ? '/admin/new' : "/admin/edit/#{@post.id}", method: 'post' }
|
2
|
+
= form_field @post, :title, placeholder: "Title of your post."
|
3
|
+
= form_field @post, :link, placeholder: "Optional link to external URL."
|
4
|
+
= form_field @post, :body, type: :textarea, placeholder: "Your post's body of text. Markdown formatting available."
|
5
|
+
= form_field @post, :summary, placeholder: "An optional summary of your post. Markdown formatting available."
|
6
|
+
= form_field @post, :read_more, label: "'Read More' Link Text", placeholder: "When supplying a summary text, this will be the link to the full post."
|
7
|
+
|
8
|
+
.row
|
9
|
+
.four.columns
|
10
|
+
= form_field @post, :slug, label: 'URL Slug', placeholder: "Your post's URL slug.", hint: @post.previous_slugs.any? ? "Previous slugs: #{@post.previous_slugs.join ', '}" : nil
|
11
|
+
.four.columns
|
12
|
+
= form_field @post, :published_at, placeholder: 'Try "now", "in 3 days", ...'
|
13
|
+
.four.columns
|
14
|
+
= form_field @post, :status, type: :dropdown, options: [[:draft, "Draft"], [:published, "Published"]]
|
15
|
+
|
16
|
+
.row
|
17
|
+
.buttons.twelve.columns
|
18
|
+
%input{ type: 'submit', value: @post.new_record? ? 'Create Post' : 'Update Post' }
|
data/lib/views/post.haml
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
// load stuff!
|
2
|
+
@import 'schnitzelstyle/complete';
|
3
|
+
|
4
|
+
/* misc crap */
|
5
|
+
.social_media_buttons {
|
6
|
+
margin: 1em 0;
|
7
|
+
}
|
8
|
+
|
9
|
+
@mixin smallish {
|
10
|
+
font: $font-footer;
|
11
|
+
font-size: 80%;
|
12
|
+
color: lighten($color-text, 30%);
|
13
|
+
a {
|
14
|
+
color: lighten($color-text, 30%);
|
15
|
+
border: none;
|
16
|
+
}
|
17
|
+
a:hover {
|
18
|
+
text-decoration: underline;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
/* posts */
|
23
|
+
article.post {
|
24
|
+
&.draft header h1 { background-color: #ccc; }
|
25
|
+
&.link header {
|
26
|
+
h1 {
|
27
|
+
font-size: 100%;
|
28
|
+
background: none;
|
29
|
+
text-transform: none;
|
30
|
+
padding: none;
|
31
|
+
}
|
32
|
+
font: $font-header;
|
33
|
+
font-weight: bold;
|
34
|
+
background-color: darken($color-page-background, 5%);
|
35
|
+
display: inline-block;
|
36
|
+
line-height: 150%;
|
37
|
+
border-bottom: 2px solid $color-link-underline;
|
38
|
+
color: darken($color-page-background, 30%);
|
39
|
+
a {
|
40
|
+
border: none;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
div.summary {
|
45
|
+
margin-bottom: 2em;
|
46
|
+
|
47
|
+
p {
|
48
|
+
font: $font-header;
|
49
|
+
font-size: 125%;
|
50
|
+
line-height: 150%;
|
51
|
+
color: #000;
|
52
|
+
|
53
|
+
@media only screen and (max-width: 640px) {
|
54
|
+
line-height: 130%;
|
55
|
+
|
56
|
+
section.posts & {
|
57
|
+
font-size: 100%;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
footer {
|
64
|
+
margin-top: 0.5em;
|
65
|
+
|
66
|
+
p.permalink {
|
67
|
+
@include smallish;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
/* admin post lists */
|
73
|
+
ul.admin-post-list {
|
74
|
+
max-height: 300px;
|
75
|
+
padding-bottom: 5px;
|
76
|
+
overflow: auto;
|
77
|
+
}
|
78
|
+
|
79
|
+
/* admin links */
|
80
|
+
ul.admin {
|
81
|
+
@include smallish;
|
82
|
+
list-style: none;
|
83
|
+
|
84
|
+
li {
|
85
|
+
display: inline;
|
86
|
+
margin-right: 0.5em;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
/* forms */
|
91
|
+
.input.post_published_at input { width: 180px; }
|
92
|
+
.input.post_slug input { width: 180px }
|
93
|
+
|
94
|
+
/* disqus */
|
95
|
+
#dsq-content {
|
96
|
+
a {
|
97
|
+
border: 0;
|
98
|
+
}
|
99
|
+
#dsq-reply {
|
100
|
+
margin-bottom: 2em;
|
101
|
+
}
|
102
|
+
h3 {
|
103
|
+
margin-top: 1em !important;
|
104
|
+
@include clearfix;
|
105
|
+
}
|
106
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/schnitzelpress/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Hendrik Mans"]
|
6
|
+
gem.email = ["hendrik@mans.de"]
|
7
|
+
gem.description = %q{A simple blog engine for sane hackers.}
|
8
|
+
gem.summary = %q{A simple blog engine for sane hackers.}
|
9
|
+
gem.homepage = "http://www.schnitzelpress.org"
|
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 = "schnitzelpress"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = SchnitzelPress::VERSION
|
17
|
+
|
18
|
+
# base dependencies
|
19
|
+
gem.add_dependency 'rack', '~> 1.4.0'
|
20
|
+
gem.add_dependency 'sinatra', '~> 1.3.2'
|
21
|
+
gem.add_dependency 'activesupport', '~> 3.2.0'
|
22
|
+
gem.add_dependency 'rack-cache'
|
23
|
+
|
24
|
+
# database related
|
25
|
+
gem.add_dependency 'mongoid', '~> 2.4'
|
26
|
+
gem.add_dependency 'bson_ext', '~> 1.5'
|
27
|
+
|
28
|
+
# authentication
|
29
|
+
gem.add_dependency 'omniauth'
|
30
|
+
gem.add_dependency 'omniauth-browserid'
|
31
|
+
|
32
|
+
# frontend/views/assets related
|
33
|
+
gem.add_dependency 'haml'
|
34
|
+
gem.add_dependency 'sass'
|
35
|
+
gem.add_dependency 'redcarpet'
|
36
|
+
gem.add_dependency 'coderay'
|
37
|
+
gem.add_dependency 'schnitzelstyle', '~> 0.0.4'
|
38
|
+
gem.add_dependency 'i18n'
|
39
|
+
gem.add_dependency 'tilt', '~> 1.3.0'
|
40
|
+
|
41
|
+
# CLI related
|
42
|
+
gem.add_dependency 'thor'
|
43
|
+
gem.add_dependency 'rake'
|
44
|
+
gem.add_dependency 'wirble'
|
45
|
+
|
46
|
+
# misc
|
47
|
+
gem.add_dependency 'chronic'
|
48
|
+
|
49
|
+
# development dependencies
|
50
|
+
gem.add_development_dependency 'rspec', '>= 2.8.0'
|
51
|
+
gem.add_development_dependency 'rspec-html-matchers'
|
52
|
+
gem.add_development_dependency 'database_cleaner'
|
53
|
+
gem.add_development_dependency 'factory_girl'
|
54
|
+
gem.add_development_dependency 'ffaker'
|
55
|
+
gem.add_development_dependency 'timecop'
|
56
|
+
gem.add_development_dependency 'shotgun'
|
57
|
+
gem.add_development_dependency 'rack-test'
|
58
|
+
gem.add_development_dependency 'watchr'
|
59
|
+
gem.add_development_dependency 'awesome_print'
|
60
|
+
end
|
data/spec/app_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestApp < SchnitzelPress::App
|
4
|
+
configure do
|
5
|
+
set :blog_title, "A Test Blog"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe SchnitzelPress::App do
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
def app
|
13
|
+
TestApp
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'the home page' do
|
17
|
+
before do
|
18
|
+
2.times { Factory(:draft_post) }
|
19
|
+
5.times { Factory(:published_post) }
|
20
|
+
get '/'
|
21
|
+
end
|
22
|
+
|
23
|
+
subject { last_response }
|
24
|
+
|
25
|
+
it { should be_ok }
|
26
|
+
its(:body) { should have_tag 'title', text: "A Test Blog" }
|
27
|
+
its(:body) { should have_tag 'section.posts > article.post.published', count: 5 }
|
28
|
+
its(:body) { should_not have_tag 'section.posts > article.post.draft' }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'the public feed url' do
|
32
|
+
before do
|
33
|
+
TestApp.set :feed_url, 'http://feeds.feedburner.com/example_org'
|
34
|
+
get '/feed'
|
35
|
+
end
|
36
|
+
|
37
|
+
subject { last_response }
|
38
|
+
it { should be_redirect }
|
39
|
+
its(:status) { should == 302 }
|
40
|
+
specify { subject["Location"].should == 'http://feeds.feedburner.com/example_org' }
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'viewing a single post' do
|
44
|
+
context 'when the post has multiple slugs' do
|
45
|
+
before do
|
46
|
+
@post = Factory(:post, slugs: ['ancient-slug', 'old-slug', 'current-slug'])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should enforce the canonical URL' do
|
50
|
+
get "/#{@post.year}/#{@post.month}/#{@post.day}/ancient-slug/"
|
51
|
+
last_response.should be_redirect
|
52
|
+
last_response["Location"].should == "http://example.org/#{@post.year}/#{@post.month}/#{@post.day}/current-slug/"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/factories.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :post, class: SchnitzelPress::Post do
|
3
|
+
title { Faker::Lorem.sentence }
|
4
|
+
body { Faker::Lorem.paragraphs }
|
5
|
+
published_at { rand(1.year).minutes.ago }
|
6
|
+
end
|
7
|
+
|
8
|
+
factory :published_post, parent: :post do
|
9
|
+
status :published
|
10
|
+
end
|
11
|
+
|
12
|
+
factory :draft_post, parent: :post do
|
13
|
+
status :draft
|
14
|
+
end
|
15
|
+
|
16
|
+
factory :page, parent: :post do
|
17
|
+
published_at nil
|
18
|
+
end
|
19
|
+
|
20
|
+
factory :published_page, parent: :page do
|
21
|
+
status :published
|
22
|
+
published_at nil
|
23
|
+
end
|
24
|
+
|
25
|
+
factory :draft_page, parent: :page do
|
26
|
+
status :draft
|
27
|
+
end
|
28
|
+
end
|
data/spec/post_spec.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SchnitzelPress::Post do
|
4
|
+
subject do
|
5
|
+
Factory.build(:post)
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'slugs' do
|
9
|
+
before do
|
10
|
+
subject.slugs = ['some-slug', 'another-slug']
|
11
|
+
subject.slug = 'a-new-slug'
|
12
|
+
end
|
13
|
+
|
14
|
+
its(:slugs) { should == ['some-slug', 'another-slug', 'a-new-slug'] }
|
15
|
+
its(:slug) { should == 'a-new-slug'}
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'saving' do
|
19
|
+
context "when no slug is set" do
|
20
|
+
before do
|
21
|
+
subject.title = "Team Schnitzel is AWESOME!"
|
22
|
+
subject.slug = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should set its slug to a sluggified version of its title" do
|
26
|
+
expect { subject.save }.to change(subject, :slug).from(nil).to('team-schnitzel-is-awesome')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when another post on the same day is already using the same slug" do
|
31
|
+
before do
|
32
|
+
@other_post = Factory(:published_post, slugs: ["amazing-slug"])
|
33
|
+
subject.published_at = @other_post.published_at
|
34
|
+
subject.slug = "amazing-slug"
|
35
|
+
end
|
36
|
+
|
37
|
+
it { should_not be_valid }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when another page is using the same slug" do
|
41
|
+
subject { Factory.build(:draft_page) }
|
42
|
+
|
43
|
+
before do
|
44
|
+
@other_page = Factory(:published_page, slugs: ["amazing-slug"])
|
45
|
+
subject.slug = "amazing-slug"
|
46
|
+
end
|
47
|
+
|
48
|
+
it { should_not be_valid }
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should store blank attributes as nil" do
|
52
|
+
subject.link = ""
|
53
|
+
expect { subject.save }.to change(subject, :link).from("").to(nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should remove leading and trailing spaces from string attributes" do
|
57
|
+
subject.link = " moo "
|
58
|
+
subject.link.should == " moo "
|
59
|
+
subject.save
|
60
|
+
subject.link.should == "moo"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '.latest' do
|
65
|
+
it 'should return the latest published posts' do
|
66
|
+
2.times { Factory :draft_post }
|
67
|
+
5.times { Factory :published_post }
|
68
|
+
SchnitzelPress::Post.latest.size.should == 5
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'date methods' do
|
73
|
+
before { subject.published_at = "2012-01-02 12:23:13" }
|
74
|
+
its(:year) { should == 2012 }
|
75
|
+
its(:month) { should == 01 }
|
76
|
+
its(:day) { should == 02 }
|
77
|
+
end
|
78
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
SPEC_DIR = File.dirname(__FILE__)
|
2
|
+
lib_path = File.expand_path("#{SPEC_DIR}/../lib")
|
3
|
+
$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'bundler/setup'
|
7
|
+
|
8
|
+
require 'schnitzelpress'
|
9
|
+
|
10
|
+
SchnitzelPress.mongo_uri = 'mongodb://localhost/_schreihals_test'
|
11
|
+
|
12
|
+
require 'awesome_print'
|
13
|
+
require 'rack/test'
|
14
|
+
require 'rspec-html-matchers'
|
15
|
+
require 'database_cleaner'
|
16
|
+
require 'ffaker'
|
17
|
+
require 'factory_girl'
|
18
|
+
require File.expand_path("../factories.rb", __FILE__)
|
19
|
+
require 'timecop'
|
20
|
+
Timecop.freeze
|
21
|
+
|
22
|
+
set :environment, :test
|
23
|
+
|
24
|
+
RSpec.configure do |config|
|
25
|
+
config.before(:suite) do
|
26
|
+
DatabaseCleaner[:mongoid].strategy = :truncation
|
27
|
+
end
|
28
|
+
|
29
|
+
config.before(:each) do
|
30
|
+
DatabaseCleaner[:mongoid].start
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after(:each) do
|
34
|
+
DatabaseCleaner[:mongoid].clean
|
35
|
+
end
|
36
|
+
end
|