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.
Files changed (51) hide show
  1. data/.gitignore +21 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +9 -0
  4. data/.watchr +15 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +5 -0
  8. data/Rakefile +7 -0
  9. data/bin/schnitzelpress +5 -0
  10. data/lib/public/.gitkeep +0 -0
  11. data/lib/public/favicon.ico +0 -0
  12. data/lib/schnitzelpress.rb +30 -0
  13. data/lib/schnitzelpress/actions/admin.rb +49 -0
  14. data/lib/schnitzelpress/actions/auth.rb +29 -0
  15. data/lib/schnitzelpress/actions/blog.rb +70 -0
  16. data/lib/schnitzelpress/app.rb +45 -0
  17. data/lib/schnitzelpress/cli.rb +28 -0
  18. data/lib/schnitzelpress/helpers.rb +72 -0
  19. data/lib/schnitzelpress/post.rb +168 -0
  20. data/lib/schnitzelpress/rake.rb +23 -0
  21. data/lib/schnitzelpress/static.rb +17 -0
  22. data/lib/schnitzelpress/version.rb +3 -0
  23. data/lib/templates/new_blog/.gitignore +5 -0
  24. data/lib/templates/new_blog/Gemfile +19 -0
  25. data/lib/templates/new_blog/Rakefile +2 -0
  26. data/lib/templates/new_blog/app.rb.tt +28 -0
  27. data/lib/templates/new_blog/config.ru.tt +2 -0
  28. data/lib/templates/new_blog/public/.gitkeep +0 -0
  29. data/lib/views/404.haml +4 -0
  30. data/lib/views/admin/admin.haml +12 -0
  31. data/lib/views/admin/edit.haml +3 -0
  32. data/lib/views/admin/new.haml +3 -0
  33. data/lib/views/atom.haml +23 -0
  34. data/lib/views/blog.scss +1 -0
  35. data/lib/views/index.haml +6 -0
  36. data/lib/views/layout.haml +25 -0
  37. data/lib/views/partials/_admin_post_list.haml +11 -0
  38. data/lib/views/partials/_disqus.haml +17 -0
  39. data/lib/views/partials/_form_field.haml +29 -0
  40. data/lib/views/partials/_gauges.haml +12 -0
  41. data/lib/views/partials/_google_analytics.haml +10 -0
  42. data/lib/views/partials/_post.haml +44 -0
  43. data/lib/views/partials/_post_form.haml +18 -0
  44. data/lib/views/post.haml +7 -0
  45. data/lib/views/schnitzelpress.scss +106 -0
  46. data/schnitzelpress.gemspec +60 -0
  47. data/spec/app_spec.rb +56 -0
  48. data/spec/factories.rb +28 -0
  49. data/spec/post_spec.rb +78 -0
  50. data/spec/spec_helper.rb +36 -0
  51. metadata +426 -0
@@ -0,0 +1,168 @@
1
+ require 'tilt'
2
+ require 'coderay'
3
+
4
+ module SchnitzelPress
5
+ class MarkdownRenderer < Redcarpet::Render::HTML
6
+ include Redcarpet::Render::SmartyPants
7
+
8
+ def block_code(code, language)
9
+ CodeRay.highlight(code, language)
10
+ end
11
+ end
12
+
13
+ class Post
14
+ include Mongoid::Document
15
+ store_in :posts
16
+
17
+ # basic data
18
+ field :title, type: String
19
+ field :body, type: String
20
+ field :slugs, type: Array, default: []
21
+
22
+ # optional fields
23
+ field :summary, type: String
24
+ field :link, type: String
25
+ field :read_more, type: String
26
+
27
+ # times & status
28
+ field :published_at, type: DateTime
29
+ field :status, type: Symbol, default: :draft
30
+
31
+ # flags
32
+ field :disqus, type: Boolean, default: false
33
+
34
+ # extra
35
+ field :body_html, type: String
36
+
37
+ validates_presence_of :status, :slug
38
+ validates_inclusion_of :status, in: [:draft, :published]
39
+
40
+ scope :published, where(:status => :published)
41
+ scope :drafts, where(:status => :draft)
42
+ scope :pages, where(:published_at.exists => false)
43
+ scope :posts, where(:published_at.exists => true)
44
+ scope :article_posts, -> { posts.where(:link => nil) }
45
+ scope :link_posts, -> { posts.where(:link.ne => nil) }
46
+ scope :for_year, ->(year) { d = Date.new(year) ; where(published_at: (d.beginning_of_year)..(d.end_of_year)) }
47
+ scope :for_month, ->(year, month) { d = Date.new(year, month) ; where(published_at: (d.beginning_of_month)..(d.end_of_month)) }
48
+ scope :for_day, ->(year, month, day) { d = Date.new(year, month, day) ; where(published_at: (d.beginning_of_day)..(d.end_of_day)) }
49
+ scope :latest, -> { published.posts.desc(:published_at) }
50
+
51
+ before_validation :nil_if_blank
52
+ before_validation :set_defaults
53
+ validate :validate_slug
54
+ before_save :update_body_html
55
+
56
+ def disqus_identifier
57
+ slug
58
+ end
59
+
60
+ def slug
61
+ slugs.try(:last)
62
+ end
63
+
64
+ def previous_slugs
65
+ slugs[0..-2]
66
+ end
67
+
68
+ def published_at=(v)
69
+ v = Chronic.parse(v) if v.is_a?(String)
70
+ super(v)
71
+ end
72
+
73
+ def slug=(v)
74
+ unless v.blank?
75
+ slugs.delete(v)
76
+ slugs << v
77
+ end
78
+ end
79
+
80
+ def set_defaults
81
+ if slug.blank? && title.present?
82
+ self.slug = title.parameterize
83
+ end
84
+ end
85
+
86
+ def validate_slug
87
+ conflicting_posts = Post.where(slugs: slug)
88
+ if published_at.present?
89
+ conflicting_posts = conflicting_posts.for_day(published_at.year, published_at.month, published_at.day)
90
+ end
91
+
92
+ if conflicting_posts.any? && conflicting_posts.first != self
93
+ errors[:slug] = "This slug is already in use by another post."
94
+ end
95
+ end
96
+
97
+ def nil_if_blank
98
+ attributes.keys.each do |attr|
99
+ self[attr].strip! if self[attr].is_a?(String)
100
+ self[attr] = nil if self[attr] == ""
101
+ end
102
+ end
103
+
104
+ def update_body_html
105
+ self.body_html = render
106
+ end
107
+
108
+ def to_html
109
+ if body_html.nil?
110
+ update_body_html
111
+ save
112
+ end
113
+
114
+ body_html
115
+ end
116
+
117
+ def render
118
+ @@markdown ||= Redcarpet::Markdown.new(MarkdownRenderer,
119
+ autolink: true, space_after_headers: true, fenced_code_blocks: true)
120
+
121
+ @@markdown.render(body)
122
+ end
123
+
124
+ def post?
125
+ published_at.present?
126
+ end
127
+
128
+ def page?
129
+ !post?
130
+ end
131
+
132
+ def published?
133
+ status == :published
134
+ end
135
+
136
+ def draft?
137
+ status == :draft
138
+ end
139
+
140
+ def link_post?
141
+ link.present?
142
+ end
143
+
144
+ def article_post?
145
+ link.nil?
146
+ end
147
+
148
+ def year
149
+ published_at.year
150
+ end
151
+
152
+ def month
153
+ published_at.month
154
+ end
155
+
156
+ def day
157
+ published_at.day
158
+ end
159
+
160
+ def to_url
161
+ published_at.present? ? "/#{year}/#{month}/#{day}/#{slug}/" : "/#{slug}/"
162
+ end
163
+
164
+ def disqus?
165
+ disqus && published?
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,23 @@
1
+ require 'schnitzelpress'
2
+
3
+ desc 'Run the SchnitzelPress console'
4
+ task :console do
5
+ require 'irb'
6
+ require 'wirble'
7
+ ARGV.clear
8
+ Wirble.init
9
+ Wirble.colorize
10
+ IRB.start
11
+ end
12
+
13
+ namespace :db do
14
+ desc 'Import Heroku database to local database'
15
+ task :pull do
16
+ system "MONGO_URL=\"#{SchnitzelPress.mongo_uri}\" heroku mongo:pull"
17
+ end
18
+
19
+ desc 'Push local database to Heroku'
20
+ task :push do
21
+ system "MONGO_URL=\"#{SchnitzelPress.mongo_uri}\" heroku mongo:push"
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module SchnitzelPress
2
+ class Static
3
+ def initialize(app, public_dir = './public')
4
+ @file = Rack::File.new(public_dir)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ status, headers, body = @file.call(env)
10
+ if status > 400
11
+ @app.call(env)
12
+ else
13
+ [status, headers, body]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module SchnitzelPress
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ .rbfu-version
3
+ .powenv
4
+ .sass-cache
5
+ tmp
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'shotgun'
5
+ gem 'heroku'
6
+ end
7
+
8
+ # For now, we're going to be using the development versions of
9
+ # schnitzelstyle and schnitzelpress. I told you you were about
10
+ # to live dangerously!
11
+ #
12
+ gem 'schnitzelstyle', git: 'git://github.com/teamschnitzel/schnitzelstyle.git'
13
+ gem 'schnitzelpress', git: 'git://github.com/teamschnitzel/schnitzelpress.git'
14
+
15
+ # If you'd prefer to use the officially released versions,
16
+ # use these instead:
17
+ #
18
+ # gem 'schnitzelstyle', :path => '../schnitzelstyle'
19
+ # gem 'schnitzelpress', :path => '../schnitzelpress'
@@ -0,0 +1,2 @@
1
+ require File.expand_path("../app.rb", __FILE__)
2
+ require 'schnitzelpress/rake'
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler.require
5
+
6
+ SchnitzelPress.mongo_uri =
7
+ ENV['MONGOLAB_URI'] ||
8
+ ENV['MONGOHQ_URL'] ||
9
+ ENV['MONGO_URL'] ||
10
+ 'mongodb://localhost/<%= @name %>' # used for local development
11
+
12
+ class App < SchnitzelPress::App
13
+ configure do
14
+ set :blog_title, "<%= @name %>"
15
+ set :blog_description, "A new blog powered by SchnitzelPress."
16
+ set :author_name, "Your Name"
17
+ set :footer, "powered by [SchnitzelPress](http://schnitzelpress.org)"
18
+ set :administrator, "browser_id:hendrik@mans.de"
19
+
20
+ # The following are optional:
21
+ #
22
+ # set :disqus_name, "..."
23
+ # set :google_analytics_id, "..."
24
+ # set :gauges_id, "..."
25
+ # set :twitter_id, '...'
26
+ # set :read_more, "Read ALL the things"
27
+ end
28
+ end
@@ -0,0 +1,2 @@
1
+ require File.expand_path("../app.rb", __FILE__)
2
+ run App
File without changes
@@ -0,0 +1,4 @@
1
+ %section
2
+ %h2 400 + 4
3
+ %p
4
+ The document you requested could not be found.
@@ -0,0 +1,12 @@
1
+ %section
2
+ %h2 Administration
3
+ %p
4
+ You're logged in as #{session[:user]}.
5
+ %ul.admin
6
+ %li
7
+ %a.green.button{href: '/admin/new'} Create new Post
8
+ %a.red.button{href: '/logout'} Logout
9
+
10
+ = partial "admin_post_list", posts: @drafts, title: "Drafts"
11
+ = partial "admin_post_list", posts: @posts, title: "Published Posts"
12
+ = partial "admin_post_list", posts: @pages, title: "Pages"
@@ -0,0 +1,3 @@
1
+ %section
2
+ %h2 Edit Post
3
+ = partial 'post_form', post: @post
@@ -0,0 +1,3 @@
1
+ %section
2
+ %h2 New Post
3
+ = partial 'post_form', post: @post
@@ -0,0 +1,23 @@
1
+ !!! XML
2
+ %feed{xmlns: 'http://www.w3.org/2005/Atom'}
3
+ %title= settings.blog_title
4
+ %link{href: url_for('/', absolute: true)}
5
+ %id= base_url
6
+ - if @posts.any?
7
+ %updated= @posts.first.published_at
8
+
9
+ %author
10
+ %name= settings.author_name
11
+
12
+ - @posts.each do |post|
13
+ %entry
14
+ %title= html_escape post.title
15
+ %link{href: url_for(post, absolute: true)}
16
+ %id= url_for(post, absolute: true)
17
+ %published= post.published_at
18
+ %updated= post.published_at
19
+ %author
20
+ %name= settings.author_name
21
+ %content{type: 'html'}
22
+ :cdata
23
+ #{post.to_html}
@@ -0,0 +1 @@
1
+ @import 'schnitzelpress';
@@ -0,0 +1,6 @@
1
+ %section.posts
2
+ - @posts.each do |post|
3
+ = partial post
4
+
5
+ %footer
6
+ %a.button{ href: "/?page=#{params[:page].to_i + 1}"} View Older Posts
@@ -0,0 +1,25 @@
1
+ !!! 5
2
+ %html
3
+ %head
4
+ %title= [@page_title, settings.blog_title].compact.join(" | ")
5
+ %meta{ :"http-equiv" => "content-type", content: "text/html; charset=UTF-8" }
6
+ %meta{ name: "viewport", content: "width=device-width, initial-scale=1.0" }
7
+ %link{ href: '/blog.css', media: "screen", rel: "stylesheet", type: "text/css" }
8
+ %link{ href: settings.feed_url, title: "Subscribe via Atom Feed", rel: 'alternate', type: 'application/atom+xml' }
9
+ %body
10
+ .container
11
+ %header
12
+ .site-title
13
+ %a{href: '/'}= settings.blog_title
14
+ - if @show_description
15
+ ~ markdown settings.blog_description
16
+
17
+ = yield
18
+
19
+ %footer
20
+ ~ markdown settings.footer
21
+
22
+ - if production? && settings.google_analytics_id.present?
23
+ = partial 'google_analytics'
24
+ - if production? && settings.gauges_id.present?
25
+ = partial 'gauges'
@@ -0,0 +1,11 @@
1
+ - if posts.any?
2
+ %h2= title
3
+ %ul.admin-post-list
4
+ - posts.each do |post|
5
+ %ul.admin
6
+ %li
7
+ %a.button{href: "/admin/edit/#{post.id}"} edit
8
+ %li
9
+ %a.button{href: url_for(post)} view
10
+ %li
11
+ = post.title || ("%s..." % html_escape(post.body.first(50)))
@@ -0,0 +1,17 @@
1
+ #disqus_thread
2
+
3
+ :javascript
4
+ /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
5
+ var disqus_shortname = '#{settings.disqus_name}';
6
+ var disqus_developer = #{production? ? 0 : 1};
7
+ var disqus_identifier = '#{disqus_identifier}';
8
+
9
+ /* * * DON'T EDIT BELOW THIS LINE * * */
10
+ (function() {
11
+ var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
12
+ dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
13
+ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
14
+ })();
15
+
16
+ %noscript
17
+ Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a>
@@ -0,0 +1,29 @@
1
+ - field_options = options.slice(:id, :name, :placeholder)
2
+
3
+ .input{ class: [options[:type], options[:class]] }
4
+ %label{ for: options[:id] } #{options[:label]}:
5
+ - case options[:type].to_sym
6
+ - when :textarea
7
+ %textarea{ id: options[:id], name: options[:name], rows: 20, placeholder: options[:placeholder] }= html_escape(options[:value])
8
+
9
+ - when :radio
10
+ - options[:options].each do |o|
11
+ .option
12
+ %input{ field_options.merge(type: 'radio', value: o, checked: options[:value] == o) }= o
13
+
14
+ - when :dropdown
15
+ %select{ field_options }
16
+ - options[:options].each do |val, text|
17
+ %option{value: val, selected: options[:value] == val}= text
18
+
19
+ - when :datetime
20
+ %input{ field_options.merge(value: options[:value].to_formatted_s(:db)) }
21
+
22
+ - else # normal inputs
23
+ %input{ field_options.merge(value: options[:value]) }
24
+
25
+ - if options[:errors]
26
+ .error= options[:errors].join(", ")
27
+
28
+ - if options[:hint]
29
+ .hint= options[:hint]
@@ -0,0 +1,12 @@
1
+ :javascript
2
+ var _gauges = _gauges || [];
3
+ (function() {
4
+ var t = document.createElement('script');
5
+ t.type = 'text/javascript';
6
+ t.async = true;
7
+ t.id = 'gauges-tracker';
8
+ t.setAttribute('data-site-id', '#{settings.gauges_id}');
9
+ t.src = '//secure.gaug.es/track.js';
10
+ var s = document.getElementsByTagName('script')[0];
11
+ s.parentNode.insertBefore(t, s);
12
+ })();