schnitzelpress 0.0.5
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 +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
|