├── .gitignore ├── .rspec ├── .travis.yml ├── .watchr ├── CHANGES.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── schnitzelpress ├── lib ├── assets │ └── js │ │ ├── jquery-1.7.1.js │ │ ├── jquery-ujs.js │ │ ├── jquery.cookie.js │ │ └── schnitzelpress.js ├── public │ ├── .gitkeep │ ├── favicon.ico │ ├── font │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.svgz │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── img │ │ └── background.png │ └── moo.txt ├── schnitzelpress.rb ├── schnitzelpress │ ├── actions │ │ ├── admin.rb │ │ ├── assets.rb │ │ ├── auth.rb │ │ └── blog.rb │ ├── app.rb │ ├── cache_control.rb │ ├── cli.rb │ ├── config.rb │ ├── env.rb │ ├── helpers.rb │ ├── markdown_renderer.rb │ ├── post.rb │ ├── static.rb │ └── version.rb ├── templates │ └── new_blog │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── Gemfile.lock.tt │ │ ├── Procfile │ │ └── config.ru.tt └── views │ ├── 404.haml │ ├── admin │ ├── admin.haml │ ├── config.haml │ ├── edit.haml │ └── new.haml │ ├── atom.haml │ ├── blog.scss │ ├── index.haml │ ├── layout.haml │ ├── login.haml │ ├── partials │ ├── _admin_post_list.haml │ ├── _disqus.haml │ ├── _form_field.haml │ ├── _gauges.haml │ ├── _google_analytics.haml │ ├── _gosquared.haml │ ├── _post.haml │ └── _post_form.haml │ ├── post.haml │ └── schnitzelpress.scss ├── schnitzelpress.gemspec └── spec ├── app_spec.rb ├── assets_spec.rb ├── factories.rb ├── post_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .rbfu-version 7 | .powenv 8 | .sass-cache 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | *.sublime* 21 | Gemfile.lock 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - 1.9.3 6 | #- jruby-19mode # JRuby in 1.9 mode 7 | # - rbx-18mode 8 | # - rbx-19mode # currently in active development, may or may not work for your project 9 | # uncomment this line if your project needs to run something other than `rake`: 10 | # script: bundle exec rspec spec 11 | -------------------------------------------------------------------------------- /.watchr: -------------------------------------------------------------------------------- 1 | def run(cmd, msg = nil) 2 | puts "=== %s" % msg if msg 3 | puts "=== %s" % cmd 4 | system cmd 5 | puts "\n" 6 | end 7 | 8 | watch("spec/.*_spec\.rb") { |m| run("bundle exec rspec %s" % m[0]) } 9 | watch("lib/schnitzelpress/(.*)\.rb") { |m| run("bundle exec rspec spec/%s_spec.rb" % m[1]) } 10 | watch('^spec/(spec_helper|factories)\.rb') { |f| run "bundle exec rake spec", "%s.rb has been modified" % f } 11 | 12 | # Ctrl-\ 13 | Signal.trap('QUIT') { run("bundle exec rake spec") } 14 | # Ctrl-C 15 | Signal.trap('INT') { abort("\nQuitting.") } 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | Upgrade Notes: 4 | 5 | * SchnitzelPress has been rebranded to Schnitzelpress (lower-case p; better now than later). If you've made any modifications to the host app's code, you'll probably need to make changes to reflect this. 6 | * The code you need to push to Heroku (or your own server) has been significantly thinned down. We're now providing a skeleton app that you can download to set up or update your blog. Find details on www.schnitzelpress.org. 7 | * Configuration is now stored in the database and can be edited from the new "Configuration" page in your Admin Panel; this obviously means that some of the stuff happening within your Schnitzelpress 0.1.x host application needs to be removed and re-entered in the web configuration. 8 | * Schnitzelpress now expects an environment variable to be present named SCHNITZELPRESS_OWNER, containing the email address of the admin user. On Heroku, you can add this through the `heroku config:add` command. 9 | 10 | Changes: 11 | 12 | * By popular request (haha), you can now delete posts. 13 | * The various available rake tasks have been moved to the `schnitzelpress` command line tool. 14 | * Most of your blog's configuration is now stored in MongoDB and can be modified from the new "Configuration" page in your the admin panel. 15 | * Post with dates now use double-digit days and months in their canonical URLs. (Your existing posts will forward to the new canonical URLs automatically.) 16 | * When logged in as an admin, you will be shown a small admin actions panel in the upper right corner of your browser, allowing you to quickly edit posts, jump to the admin section, or log out. 17 | * Schnitzelpress now has a light-weight, custom-built asset pipeline that serves all Javascripts and Stylesheets as one single file each, compressed and ready for hardcore caching. 18 | * When running Schnitzelpress locally (aka: development mode), you can use a simple developer-only login provider to log into your blog for testing purposes. 19 | * Various performance improvements. 20 | 21 | ## 0.1.1 (2012-02-26) 22 | 23 | * Add improved caching of post views and post indices. 24 | * Minor bugfixes. 25 | 26 | ## 0.1.0 (2012-02-25) 27 | 28 | * Initial Release 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in schnitzelpress.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Hendrik Mans 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schnitzelpress [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | ## A lean, mean blogging machine for hackers and fools. 4 | 5 | ### http://schnitzelpress.org 6 | 7 | [![Build Status](https://secure.travis-ci.org/teamschnitzel/schnitzelpress.png?branch=master)](http://travis-ci.org/teamschnitzel/schnitzelpress) 8 | 9 | ### [Sites powered by Schnitzelpress](https://github.com/hmans/schnitzelpress/wiki/Sites-powered-by-Schnitzelpress) 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | # integrate rspec 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new('spec') 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /bin/schnitzelpress: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "schnitzelpress/cli" 4 | 5 | Schnitzelpress::Cli.start 6 | -------------------------------------------------------------------------------- /lib/assets/js/jquery-ujs.js: -------------------------------------------------------------------------------- 1 | (function($, undefined) { 2 | 3 | /** 4 | * Unobtrusive scripting adapter for jQuery 5 | * 6 | * Requires jQuery 1.6.0 or later. 7 | * https://github.com/rails/jquery-ujs 8 | 9 | * Uploading file using rails.js 10 | * ============================= 11 | * 12 | * By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields 13 | * in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means. 14 | * 15 | * The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish. 16 | * 17 | * Ex: 18 | * $('form').live('ajax:aborted:file', function(event, elements){ 19 | * // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`. 20 | * // Returning false in this handler tells rails.js to disallow standard form submission 21 | * return false; 22 | * }); 23 | * 24 | * The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value. 25 | * 26 | * Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use 27 | * techniques like the iframe method to upload the file instead. 28 | * 29 | * Required fields in rails.js 30 | * =========================== 31 | * 32 | * If any blank required inputs (required="required") are detected in the remote form, the whole form submission 33 | * is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission. 34 | * 35 | * The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs. 36 | * 37 | * !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never 38 | * get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior. 39 | * 40 | * Ex: 41 | * $('form').live('ajax:aborted:required', function(event, elements){ 42 | * // Returning false in this handler tells rails.js to submit the form anyway. 43 | * // The blank required inputs are passed to this function in `elements`. 44 | * return ! confirm("Would you like to submit the form with missing info?"); 45 | * }); 46 | */ 47 | 48 | // Shorthand to make it a little easier to call public rails functions from within rails.js 49 | var rails; 50 | 51 | $.rails = rails = { 52 | // Link elements bound by jquery-ujs 53 | linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', 54 | 55 | // Select elements bound by jquery-ujs 56 | inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', 57 | 58 | // Form elements bound by jquery-ujs 59 | formSubmitSelector: 'form', 60 | 61 | // Form input elements bound by jquery-ujs 62 | formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])', 63 | 64 | // Form input elements disabled during form submission 65 | disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', 66 | 67 | // Form input elements re-enabled after form submission 68 | enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', 69 | 70 | // Form required input elements 71 | requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', 72 | 73 | // Form file input elements 74 | fileInputSelector: 'input:file', 75 | 76 | // Link onClick disable selector with possible reenable after remote submission 77 | linkDisableSelector: 'a[data-disable-with]', 78 | 79 | // Make sure that every Ajax request sends the CSRF token 80 | CSRFProtection: function(xhr) { 81 | var token = $('meta[name="csrf-token"]').attr('content'); 82 | if (token) xhr.setRequestHeader('X-CSRF-Token', token); 83 | }, 84 | 85 | // Triggers an event on an element and returns false if the event result is false 86 | fire: function(obj, name, data) { 87 | var event = $.Event(name); 88 | obj.trigger(event, data); 89 | return event.result !== false; 90 | }, 91 | 92 | // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm 93 | confirm: function(message) { 94 | return confirm(message); 95 | }, 96 | 97 | // Default ajax function, may be overridden with custom function in $.rails.ajax 98 | ajax: function(options) { 99 | return $.ajax(options); 100 | }, 101 | 102 | // Submits "remote" forms and links with ajax 103 | handleRemote: function(element) { 104 | var method, url, data, 105 | crossDomain = element.data('cross-domain') || null, 106 | dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType), 107 | options; 108 | 109 | if (rails.fire(element, 'ajax:before')) { 110 | 111 | if (element.is('form')) { 112 | method = element.attr('method'); 113 | url = element.attr('action'); 114 | data = element.serializeArray(); 115 | // memoized value from clicked submit button 116 | var button = element.data('ujs:submit-button'); 117 | if (button) { 118 | data.push(button); 119 | element.data('ujs:submit-button', null); 120 | } 121 | } else if (element.is(rails.inputChangeSelector)) { 122 | method = element.data('method'); 123 | url = element.data('url'); 124 | data = element.serialize(); 125 | if (element.data('params')) data = data + "&" + element.data('params'); 126 | } else { 127 | method = element.data('method'); 128 | url = element.attr('href'); 129 | data = element.data('params') || null; 130 | } 131 | 132 | options = { 133 | type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain, 134 | // stopping the "ajax:beforeSend" event will cancel the ajax request 135 | beforeSend: function(xhr, settings) { 136 | if (settings.dataType === undefined) { 137 | xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); 138 | } 139 | return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); 140 | }, 141 | success: function(data, status, xhr) { 142 | element.trigger('ajax:success', [data, status, xhr]); 143 | }, 144 | complete: function(xhr, status) { 145 | element.trigger('ajax:complete', [xhr, status]); 146 | }, 147 | error: function(xhr, status, error) { 148 | element.trigger('ajax:error', [xhr, status, error]); 149 | } 150 | }; 151 | // Only pass url to `ajax` options if not blank 152 | if (url) { options.url = url; } 153 | 154 | return rails.ajax(options); 155 | } else { 156 | return false; 157 | } 158 | }, 159 | 160 | // Handles "data-method" on links such as: 161 | // Delete 162 | handleMethod: function(link) { 163 | var href = link.attr('href'), 164 | method = link.data('method'), 165 | target = link.attr('target'), 166 | csrf_token = $('meta[name=csrf-token]').attr('content'), 167 | csrf_param = $('meta[name=csrf-param]').attr('content'), 168 | form = $('
'), 169 | metadata_input = ''; 170 | 171 | if (csrf_param !== undefined && csrf_token !== undefined) { 172 | metadata_input += ''; 173 | } 174 | 175 | if (target) { form.attr('target', target); } 176 | 177 | form.hide().append(metadata_input).appendTo('body'); 178 | form.submit(); 179 | }, 180 | 181 | /* Disables form elements: 182 | - Caches element value in 'ujs:enable-with' data store 183 | - Replaces element text with value of 'data-disable-with' attribute 184 | - Sets disabled property to true 185 | */ 186 | disableFormElements: function(form) { 187 | form.find(rails.disableSelector).each(function() { 188 | var element = $(this), method = element.is('button') ? 'html' : 'val'; 189 | element.data('ujs:enable-with', element[method]()); 190 | element[method](element.data('disable-with')); 191 | element.prop('disabled', true); 192 | }); 193 | }, 194 | 195 | /* Re-enables disabled form elements: 196 | - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) 197 | - Sets disabled property to false 198 | */ 199 | enableFormElements: function(form) { 200 | form.find(rails.enableSelector).each(function() { 201 | var element = $(this), method = element.is('button') ? 'html' : 'val'; 202 | if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); 203 | element.prop('disabled', false); 204 | }); 205 | }, 206 | 207 | /* For 'data-confirm' attribute: 208 | - Fires `confirm` event 209 | - Shows the confirmation dialog 210 | - Fires the `confirm:complete` event 211 | 212 | Returns `true` if no function stops the chain and user chose yes; `false` otherwise. 213 | Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. 214 | Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function 215 | return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. 216 | */ 217 | allowAction: function(element) { 218 | var message = element.data('confirm'), 219 | answer = false, callback; 220 | if (!message) { return true; } 221 | 222 | if (rails.fire(element, 'confirm')) { 223 | answer = rails.confirm(message); 224 | callback = rails.fire(element, 'confirm:complete', [answer]); 225 | } 226 | return answer && callback; 227 | }, 228 | 229 | // Helper function which checks for blank inputs in a form that match the specified CSS selector 230 | blankInputs: function(form, specifiedSelector, nonBlank) { 231 | var inputs = $(), input, 232 | selector = specifiedSelector || 'input,textarea'; 233 | form.find(selector).each(function() { 234 | input = $(this); 235 | // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs 236 | if (nonBlank ? input.val() : !input.val()) { 237 | inputs = inputs.add(input); 238 | } 239 | }); 240 | return inputs.length ? inputs : false; 241 | }, 242 | 243 | // Helper function which checks for non-blank inputs in a form that match the specified CSS selector 244 | nonBlankInputs: function(form, specifiedSelector) { 245 | return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank 246 | }, 247 | 248 | // Helper function, needed to provide consistent behavior in IE 249 | stopEverything: function(e) { 250 | $(e.target).trigger('ujs:everythingStopped'); 251 | e.stopImmediatePropagation(); 252 | return false; 253 | }, 254 | 255 | // find all the submit events directly bound to the form and 256 | // manually invoke them. If anyone returns false then stop the loop 257 | callFormSubmitBindings: function(form, event) { 258 | var events = form.data('events'), continuePropagation = true; 259 | if (events !== undefined && events['submit'] !== undefined) { 260 | $.each(events['submit'], function(i, obj){ 261 | if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event); 262 | }); 263 | } 264 | return continuePropagation; 265 | }, 266 | 267 | // replace element's html with the 'data-disable-with' after storing original html 268 | // and prevent clicking on it 269 | disableElement: function(element) { 270 | element.data('ujs:enable-with', element.html()); // store enabled state 271 | element.html(element.data('disable-with')); // set to disabled state 272 | element.bind('click.railsDisable', function(e) { // prevent further clicking 273 | return rails.stopEverything(e) 274 | }); 275 | }, 276 | 277 | // restore element to its original state which was disabled by 'disableElement' above 278 | enableElement: function(element) { 279 | if (element.data('ujs:enable-with') !== undefined) { 280 | element.html(element.data('ujs:enable-with')); // set to old enabled state 281 | // this should be element.removeData('ujs:enable-with') 282 | // but, there is currently a bug in jquery which makes hyphenated data attributes not get removed 283 | element.data('ujs:enable-with', false); // clean up cache 284 | } 285 | element.unbind('click.railsDisable'); // enable element 286 | } 287 | 288 | }; 289 | 290 | $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); 291 | 292 | $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() { 293 | rails.enableElement($(this)); 294 | }); 295 | 296 | $(document).delegate(rails.linkClickSelector, 'click.rails', function(e) { 297 | var link = $(this), method = link.data('method'), data = link.data('params'); 298 | if (!rails.allowAction(link)) return rails.stopEverything(e); 299 | 300 | if (link.is(rails.linkDisableSelector)) rails.disableElement(link); 301 | 302 | if (link.data('remote') !== undefined) { 303 | if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; } 304 | 305 | if (rails.handleRemote(link) === false) { rails.enableElement(link); } 306 | return false; 307 | 308 | } else if (link.data('method')) { 309 | rails.handleMethod(link); 310 | return false; 311 | } 312 | }); 313 | 314 | $(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) { 315 | var link = $(this); 316 | if (!rails.allowAction(link)) return rails.stopEverything(e); 317 | 318 | rails.handleRemote(link); 319 | return false; 320 | }); 321 | 322 | $(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) { 323 | var form = $(this), 324 | remote = form.data('remote') !== undefined, 325 | blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), 326 | nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); 327 | 328 | if (!rails.allowAction(form)) return rails.stopEverything(e); 329 | 330 | // skip other logic when required values are missing or file upload is present 331 | if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { 332 | return rails.stopEverything(e); 333 | } 334 | 335 | if (remote) { 336 | if (nonBlankFileInputs) { 337 | return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); 338 | } 339 | 340 | // If browser does not support submit bubbling, then this live-binding will be called before direct 341 | // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. 342 | if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e); 343 | 344 | rails.handleRemote(form); 345 | return false; 346 | 347 | } else { 348 | // slight timeout so that the submit button gets properly serialized 349 | setTimeout(function(){ rails.disableFormElements(form); }, 13); 350 | } 351 | }); 352 | 353 | $(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) { 354 | var button = $(this); 355 | 356 | if (!rails.allowAction(button)) return rails.stopEverything(event); 357 | 358 | // register the pressed submit button 359 | var name = button.attr('name'), 360 | data = name ? {name:name, value:button.val()} : null; 361 | 362 | button.closest('form').data('ujs:submit-button', data); 363 | }); 364 | 365 | $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) { 366 | if (this == event.target) rails.disableFormElements($(this)); 367 | }); 368 | 369 | $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { 370 | if (this == event.target) rails.enableFormElements($(this)); 371 | }); 372 | 373 | })( jQuery ); 374 | -------------------------------------------------------------------------------- /lib/assets/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2011, Klaus Hartl 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.opensource.org/licenses/GPL-2.0 9 | */ 10 | (function($) { 11 | $.cookie = function(key, value, options) { 12 | 13 | // key and at least value given, set cookie... 14 | if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) { 15 | options = $.extend({}, options); 16 | 17 | if (value === null || value === undefined) { 18 | options.expires = -1; 19 | } 20 | 21 | if (typeof options.expires === 'number') { 22 | var days = options.expires, t = options.expires = new Date(); 23 | t.setDate(t.getDate() + days); 24 | } 25 | 26 | value = String(value); 27 | 28 | return (document.cookie = [ 29 | encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), 30 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 31 | options.path ? '; path=' + options.path : '', 32 | options.domain ? '; domain=' + options.domain : '', 33 | options.secure ? '; secure' : '' 34 | ].join('')); 35 | } 36 | 37 | // key and possibly options given, get cookie... 38 | options = value || {}; 39 | var decode = options.raw ? function(s) { return s; } : decodeURIComponent; 40 | 41 | var pairs = document.cookie.split('; '); 42 | for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) { 43 | if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined 44 | } 45 | return null; 46 | }; 47 | })(jQuery); 48 | -------------------------------------------------------------------------------- /lib/assets/js/schnitzelpress.js: -------------------------------------------------------------------------------- 1 | function loginViaEmail() { 2 | navigator.id.getVerifiedEmail(function(assertion) { 3 | if (assertion) { 4 | $('input[name=assertion]').val(assertion); 5 | $('form').submit(); 6 | } else { 7 | window.location = "/auth/failure"; 8 | } 9 | }); 10 | } 11 | 12 | $(document).ready(function() { 13 | $('form').submit(function(evt) { 14 | $('html').addClass('loading'); 15 | }); 16 | 17 | $('a#browser_id').click(function(evt) { 18 | evt.preventDefault(); 19 | loginViaEmail(); 20 | }); 21 | 22 | var showAdmin = $.cookie('show_admin'); 23 | if (showAdmin) { 24 | $('body').addClass('show_admin'); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /lib/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/.gitkeep -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/favicon.ico -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Designer : Dave Gandy 7 | Foundry : Fort Awesome 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.svgz -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /lib/public/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/img/background.png -------------------------------------------------------------------------------- /lib/public/moo.txt: -------------------------------------------------------------------------------- 1 | moo from schnitzelpress 2 | -------------------------------------------------------------------------------- /lib/schnitzelpress.rb: -------------------------------------------------------------------------------- 1 | require 'schnitzelpress/version' 2 | 3 | require 'sinatra' 4 | require 'haml' 5 | require 'sass' 6 | require 'redcarpet' 7 | require 'schnitzelstyle' 8 | require 'rack/contrib' 9 | require 'rack/cache' 10 | require 'mongoid' 11 | require 'chronic' 12 | 13 | require 'active_support/inflector' 14 | require 'active_support/core_ext/class' 15 | require 'active_support/concern' 16 | 17 | require 'schnitzelpress/cache_control' 18 | require 'schnitzelpress/env' 19 | require 'schnitzelpress/static' 20 | require 'schnitzelpress/helpers' 21 | require 'schnitzelpress/markdown_renderer' 22 | require 'schnitzelpress/config' 23 | require 'schnitzelpress/post' 24 | require 'schnitzelpress/actions/assets' 25 | require 'schnitzelpress/actions/blog' 26 | require 'schnitzelpress/actions/auth' 27 | require 'schnitzelpress/actions/admin' 28 | require 'schnitzelpress/app' 29 | 30 | Sass::Engine::DEFAULT_OPTIONS[:load_paths].unshift(File.expand_path("../views", __FILE__)) 31 | Sass::Engine::DEFAULT_OPTIONS[:load_paths].unshift(File.expand_path("./views")) 32 | 33 | Mongoid.logger.level = 3 34 | 35 | module Schnitzelpress 36 | mattr_reader :mongo_uri 37 | 38 | class << self 39 | def mongo_uri=(uri) 40 | Mongoid::Config.from_hash("uri" => uri) 41 | Schnitzelpress::Post.create_indexes 42 | @@mongo_uri = uri 43 | end 44 | 45 | def init! 46 | # Mongoid.load!("./config/mongo.yml") 47 | if mongo_uri = ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL'] || ENV['MONGO_URL'] 48 | self.mongo_uri = mongo_uri 49 | else 50 | raise "Please set MONGO_URL, MONGOHQ_URL or MONGOLAB_URI to your MongoDB connection string." 51 | end 52 | Schnitzelpress::Post.create_indexes 53 | end 54 | 55 | def omnomnom! 56 | init! 57 | App.with_local_files 58 | end 59 | end 60 | end 61 | 62 | # teach HAML to use RedCarpet for markdown 63 | module Haml::Filters::Redcarpet 64 | include Haml::Filters::Base 65 | 66 | def render(text) 67 | Redcarpet::Markdown.new(Schnitzelpress::MarkdownRenderer, 68 | :autolink => true, :space_after_headers => true, :fenced_code_blocks => true). 69 | render(text) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/admin.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Actions 3 | module Admin 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before '/admin/?*' do 8 | admin_only! 9 | end 10 | 11 | get '/admin/?' do 12 | @posts = Post.published.posts.desc(:published_at) 13 | @pages = Post.published.pages 14 | @drafts = Post.drafts 15 | haml :'admin/admin' 16 | end 17 | 18 | get '/admin/config/?' do 19 | haml :'admin/config' 20 | end 21 | 22 | post '/admin/config' do 23 | config.attributes = params[:config] 24 | if config.save 25 | CacheControl.bust! 26 | redirect '/admin' 27 | else 28 | haml :'admin/config' 29 | end 30 | end 31 | 32 | get '/admin/new/?' do 33 | @post = Post.new 34 | haml :'admin/new' 35 | end 36 | 37 | post '/admin/new/?' do 38 | @post = Post.new(params[:post]) 39 | if @post.save 40 | redirect url_for(@post) 41 | else 42 | haml :'admin/new' 43 | end 44 | end 45 | 46 | get '/admin/edit/:id/?' do 47 | @post = Post.find(params[:id]) 48 | haml :'admin/edit' 49 | end 50 | 51 | put '/admin/edit/:id/?' do 52 | @post = Post.find(params[:id]) 53 | @post.attributes = params[:post] 54 | if @post.save 55 | redirect url_for(@post) 56 | else 57 | haml :'admin/edit' 58 | end 59 | end 60 | 61 | delete '/admin/edit/:id/?' do 62 | @post = Post.find(params[:id]) 63 | @post.destroy 64 | redirect '/admin' 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/assets.rb: -------------------------------------------------------------------------------- 1 | require 'packr' 2 | 3 | 4 | module Schnitzelpress 5 | class JavascriptPacker 6 | def self.pack_javascripts!(files) 7 | plain = files.map do |filename| 8 | File.read(File.expand_path("../lib/assets/js/#{filename}", settings.root)) 9 | end.join("\n") 10 | 11 | Packr.pack(plain) 12 | end 13 | end 14 | 15 | module Actions 16 | module Assets 17 | extend ActiveSupport::Concern 18 | 19 | ASSET_TIMESTAMP = Time.now.to_i 20 | JAVASCRIPT_ASSETS = ['jquery-1.7.1.js', 'jquery.cookie.js', 'schnitzelpress.js', 'jquery-ujs.js'] 21 | 22 | included do 23 | get '/assets/schnitzelpress.:timestamp.css' do 24 | cache_control :public, :max_age => 1.year.to_i 25 | scss :blog 26 | end 27 | 28 | get '/assets/schnitzelpress.:timestamp.js' do 29 | cache_control :public, :max_age => 1.year.to_i 30 | content_type 'text/javascript; charset=utf-8' 31 | JavascriptPacker.pack_javascripts!(JAVASCRIPT_ASSETS) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/auth.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth' 2 | require 'omniauth-browserid' 3 | 4 | module Schnitzelpress 5 | module Actions 6 | module Auth 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | use OmniAuth::Builder do 11 | provider :browser_id 12 | if Schnitzelpress.env.development? 13 | provider :developer , :fields => [:email], :uid_field => :email 14 | end 15 | end 16 | 17 | post '/auth/:provider/callback' do 18 | auth = request.env['omniauth.auth'] 19 | session[:auth] = {:provider => auth['provider'], :uid => auth['uid']} 20 | 21 | if admin_logged_in? 22 | response.set_cookie('show_admin', :value => true, :path => '/') 23 | redirect '/admin/' 24 | else 25 | redirect '/' 26 | end 27 | end 28 | 29 | get '/login' do 30 | haml :'login' 31 | end 32 | 33 | get '/logout' do 34 | session[:auth] = nil 35 | response.delete_cookie('show_admin') 36 | 37 | redirect '/login' 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/blog.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Actions 3 | module Blog 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | get '/' do 8 | @show_description = true 9 | if @post = Post.published.pages.where(:slugs => 'home').first 10 | extra_posts = Post.latest.limit(5) 11 | @extra_posts = ['From the Blog:', extra_posts] if extra_posts.any? 12 | render_post 13 | else 14 | render_blog 15 | end 16 | end 17 | 18 | get '/blog/?' do 19 | @show_description = true 20 | render_blog 21 | end 22 | 23 | def render_blog 24 | total_count = Post.latest.count 25 | skipped_count = params[:page].to_i * 10 26 | @posts = Post.latest.skip(skipped_count).limit(10) 27 | 28 | displayed_count = @posts.count(true) 29 | @show_previous_posts_button = total_count > skipped_count + displayed_count 30 | 31 | render_posts 32 | end 33 | 34 | # /posts.atom is now deprecated. 35 | get '/posts.atom' do 36 | redirect '/blog.atom', 301 37 | end 38 | 39 | get '/blog.atom' do 40 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 3.minutes.to_i 41 | 42 | @posts = Post.latest.limit(10) 43 | content_type 'application/atom+xml; charset=utf-8' 44 | haml :atom, :format => :xhtml, :layout => false 45 | end 46 | 47 | get '/feed/?' do 48 | redirect config.blog_feed_url, 307 49 | end 50 | 51 | get %r{^/(\d{4})/(\d{1,2})/(\d{1,2})/?$} do 52 | year, month, day = params[:captures] 53 | @posts = Post.latest.for_day(year.to_i, month.to_i, day.to_i) 54 | render_posts 55 | end 56 | 57 | get %r{^/(\d{4})/(\d{1,2})/?$} do 58 | year, month = params[:captures] 59 | @posts = Post.latest.for_month(year.to_i, month.to_i) 60 | render_posts 61 | end 62 | 63 | get %r{^/(\d{4})/?$} do 64 | year = params[:captures].first 65 | @posts = Post.latest.for_year(year.to_i) 66 | render_posts 67 | end 68 | 69 | get '/:year/:month/:day/:slug/?' do |year, month, day, slug| 70 | @post = Post. 71 | for_day(year.to_i, month.to_i, day.to_i). 72 | where(:slugs => slug).first 73 | 74 | render_post 75 | end 76 | 77 | get '/*/?' do 78 | slug = params[:splat].first 79 | @post = Post.where(:slugs => slug).first 80 | render_post 81 | end 82 | 83 | def render_post(enforce_canonical_url = true) 84 | if @post 85 | # enforce canonical URL 86 | if enforce_canonical_url && request.path != url_for(@post) 87 | redirect url_for(@post) 88 | else 89 | fresh_when :last_modified => @post.updated_at, 90 | :etag => CacheControl.etag(@post.updated_at) 91 | 92 | @show_description = @post.home_page? 93 | 94 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 60 95 | haml :post 96 | end 97 | else 98 | halt 404 99 | end 100 | end 101 | 102 | def render_posts 103 | if freshest_post = @posts.where(:updated_at.ne => nil).desc(:updated_at).first 104 | fresh_when :last_modified => freshest_post.updated_at, 105 | :etag => CacheControl.etag(freshest_post.updated_at) 106 | end 107 | 108 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 60 109 | haml :index 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/schnitzelpress/app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/content_for" 2 | 3 | module Schnitzelpress 4 | class App < Sinatra::Base 5 | STATIC_PATHS = ["/favicon.ico", "/img", "/js"] 6 | 7 | set :views, ['./views/', File.expand_path('../../views/', __FILE__)] 8 | set :public_folder, File.expand_path('../../public/', __FILE__) 9 | 10 | use Rack::Cache if Schnitzelpress.env.production? 11 | use Rack::ShowExceptions 12 | use Rack::StaticCache, 13 | :urls => STATIC_PATHS, 14 | :root => File.expand_path('../../public/', __FILE__) 15 | use Rack::MethodOverride 16 | use Rack::Session::Cookie 17 | 18 | helpers Sinatra::ContentFor 19 | helpers Schnitzelpress::Helpers 20 | include Rack::Utils 21 | include Schnitzelpress::Actions::Auth 22 | include Schnitzelpress::Actions::Assets 23 | include Schnitzelpress::Actions::Admin 24 | include Schnitzelpress::Actions::Blog 25 | 26 | configure do 27 | disable :protection 28 | set :logging, true 29 | end 30 | 31 | before do 32 | # Reload configuration before every request. I know this isn't ideal, 33 | # but right now it's the easiest way to get the configuration in synch 34 | # across multiple instances of the app. 35 | # 36 | Config.instance.reload 37 | end 38 | 39 | def fresh_when(options) 40 | last_modified options[:last_modified] 41 | etag options[:etag] 42 | end 43 | 44 | not_found do 45 | haml :"404" 46 | end 47 | 48 | def self.with_local_files 49 | Rack::Cascade.new([ 50 | Rack::StaticCache.new(self, :urls => STATIC_PATHS, :root => './public'), 51 | self 52 | ]) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/schnitzelpress/cache_control.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module CacheControl 3 | class << self 4 | def timestamp 5 | Schnitzelpress::Config.get 'cache_timestamp' 6 | end 7 | 8 | def bust! 9 | Schnitzelpress::Config.set 'cache_timestamp', Time.now 10 | end 11 | 12 | def etag(*args) 13 | Digest::MD5.hexdigest("-#{timestamp.to_i}-#{args.join '-'}-") 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/schnitzelpress/cli.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | 3 | module Schnitzelpress 4 | class Cli < Thor 5 | include Thor::Actions 6 | 7 | source_root(File.expand_path('../../templates', __FILE__)) 8 | 9 | desc "create NAME", "Creates a new Schnitzelpress blog." 10 | method_option :git, :aliases => "-g", :default => false, :type => :boolean, 11 | :desc => "Initialize a git repository in your blog's directory." 12 | 13 | def create(name) 14 | @name = name 15 | self.destination_root = name 16 | directory 'new_blog', '.' 17 | 18 | in_root do 19 | if options[:git] 20 | run "git init" 21 | run "git add ." 22 | run "git commit -m 'Created new Schnitzelpress blog'" 23 | end 24 | end 25 | end 26 | 27 | desc "update", "Update your blog's bundled Schnitzelpress version." 28 | def update 29 | run "bundle update schnitzelpress" 30 | end 31 | 32 | desc "console", "Run the Schnitzelpress console." 33 | def console 34 | require 'schnitzelpress' 35 | require 'pry' 36 | Schnitzelpress.init! 37 | ARGV.clear 38 | pry Schnitzelpress 39 | end 40 | 41 | desc "mongo_pull", "Pulls contents of remote MongoDB into your local MongoDB" 42 | def mongo_pull 43 | abort "Please set MONGO_URL." unless ENV['MONGO_URL'] 44 | system "heroku mongo:pull" 45 | end 46 | 47 | desc "mongo_push", "Pushes contents of your local MongoDB to remote MongoDB" 48 | def mongo_push 49 | abort "Please set MONGO_URL." unless ENV['MONGO_URL'] 50 | system "heroku mongo:push" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/schnitzelpress/config.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | class Config 3 | include Mongoid::Document 4 | include Mongoid::Timestamps 5 | identity :type => String 6 | store_in :config 7 | 8 | field :blog_title, :type => String, :default => "A New Schnitzelpress Blog" 9 | field :blog_description, :type => String, :default => "" 10 | field :blog_footer, :type => String, :default => "powered by [Schnitzelpress](http://schnitzelpress.org)" 11 | field :blog_feed_url, :type => String, :default => "/blog.atom" 12 | 13 | field :author_name, :type => String, :default => "Joe Schnitzel" 14 | 15 | field :disqus_id, :type => String 16 | field :google_analytics_id, :type => String 17 | field :gauges_id, :type => String 18 | field :gosquared_id, :type => String 19 | field :twitter_id, :type => String 20 | 21 | field :cache_timestamp, :type => DateTime 22 | 23 | validates :blog_title, :author_name, :presence => true 24 | 25 | class << self 26 | def instance 27 | @@instance ||= find_or_create_by(:id => 'schnitzelpress') 28 | end 29 | 30 | def forget_instance 31 | @@instance = nil 32 | end 33 | 34 | def get(k) 35 | instance.send(k) 36 | end 37 | 38 | def set(k, v) 39 | instance.update_attributes!(k => v) 40 | v 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/schnitzelpress/env.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | def self.env 3 | (ENV['RACK_ENV'] || 'development').inquiry 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/schnitzelpress/helpers.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Helpers 3 | def h(*args) 4 | escape_html(*args) 5 | end 6 | 7 | def find_template(views, name, engine, &block) 8 | Array(views).each { |v| super(v, name, engine, &block) } 9 | end 10 | 11 | def base_url 12 | "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/" 13 | end 14 | 15 | def partial(thing, locals = {}) 16 | name = case thing 17 | when String then thing 18 | else thing.class.to_s.demodulize.underscore 19 | end 20 | 21 | haml :"partials/_#{name}", :locals => { name.to_sym => thing }.merge(locals) 22 | end 23 | 24 | def config 25 | Schnitzelpress::Config.instance 26 | end 27 | 28 | def set_page_title(title) 29 | @page_title = title 30 | end 31 | 32 | def url_for(thing, options = {}) 33 | url = thing.respond_to?(:to_url) ? thing.to_url : thing.to_s 34 | url = "#{base_url.sub(/\/$/, '')}#{url}" if options[:absolute] 35 | url 36 | end 37 | 38 | def show_disqus? 39 | config.disqus_id.present? 40 | end 41 | 42 | def production? 43 | settings.environment.to_sym == :production 44 | end 45 | 46 | def user_logged_in? 47 | session[:auth].present? 48 | end 49 | 50 | def admin_logged_in? 51 | user_logged_in? && (session[:auth][:uid] == ENV['SCHNITZELPRESS_OWNER']) 52 | end 53 | 54 | def admin_only! 55 | redirect '/login' unless admin_logged_in? 56 | end 57 | 58 | def form_field(object, attribute, options = {}) 59 | options = { 60 | :label => attribute.to_s.humanize.titleize, 61 | :value => object.send(attribute), 62 | :errors => object.errors[attribute.to_sym], 63 | :class_name => object.class.to_s.demodulize.underscore 64 | }.merge(options) 65 | 66 | options[:name] ||= "#{options[:class_name]}[#{attribute}]" 67 | options[:id] ||= object.new_record? ? 68 | "new_#{options[:class_name]}_#{attribute}" : 69 | "#{options[:class_name]}_#{object.id}_#{attribute}" 70 | options[:class] ||= "#{options[:class_name]}_#{attribute}" 71 | 72 | options[:type] ||= case options[:value] 73 | when DateTime, Time, Date then :datetime 74 | when Boolean, FalseClass, TrueClass then :boolean 75 | else :text 76 | end 77 | 78 | partial 'form_field', :object => object, :attribute => attribute, :options => options 79 | end 80 | 81 | def icon(name) 82 | map = { 83 | 'glass' => 'f000', 84 | 'music' => 'f001', 85 | 'search' => 'f002', 86 | 'envelope' => 'f003', 87 | 'heart' => 'f004', 88 | 'star' => 'f005', 89 | 'star-empty' => 'f006', 90 | 'user' => 'f007', 91 | 'film' => 'f008', 92 | 'th-large' => 'f009', 93 | 'th' => 'f00a', 94 | 'th-list' => 'f00b', 95 | 'ok' => 'f00c', 96 | 'remove' => 'f00d', 97 | 'zoom-in' => 'f00e', 98 | 'zoom-out' => 'f010', 99 | 'off' => 'f011', 100 | 'signal' => 'f012', 101 | 'cog' => 'f013', 102 | 'trash' => 'f014', 103 | 'home' => 'f015', 104 | 'file' => 'f016', 105 | 'time' => 'f017', 106 | 'road' => 'f018', 107 | 'download-alt' => 'f019', 108 | 'download' => 'f01a', 109 | 'upload' => 'f01b', 110 | 'inbox' => 'f01c', 111 | 'play-circle' => 'f01d', 112 | 'repeat' => 'f01e', 113 | 'refresh' => 'f021', 114 | 'list-alt' => 'f022', 115 | 'lock' => 'f023', 116 | 'flag' => 'f024', 117 | 'headphones' => 'f025', 118 | 'volume-off' => 'f026', 119 | 'volume-down' => 'f027', 120 | 'volume-up' => 'f028', 121 | 'qrcode' => 'f029', 122 | 'barcode' => 'f02a', 123 | 'tag' => 'f02b', 124 | 'tags' => 'f02c', 125 | 'book' => 'f02d', 126 | 'bookmark' => 'f02e', 127 | 'print' => 'f02f', 128 | 'camera' => 'f030', 129 | 'font' => 'f031', 130 | 'bold' => 'f032', 131 | 'italic' => 'f033', 132 | 'text-height' => 'f034', 133 | 'text-width' => 'f035', 134 | 'align-left' => 'f036', 135 | 'align-center' => 'f037', 136 | 'align-right' => 'f038', 137 | 'align-justify' => 'f039', 138 | 'list' => 'f03a', 139 | 'indent-left' => 'f03b', 140 | 'indent-right' => 'f03c', 141 | 'facetime-video' => 'f03d', 142 | 'picture' => 'f03e', 143 | 'pencil' => 'f040', 144 | 'map-marker' => 'f041', 145 | 'adjust' => 'f042', 146 | 'tint' => 'f043', 147 | 'edit' => 'f044', 148 | 'share' => 'f045', 149 | 'check' => 'f046', 150 | 'move' => 'f047', 151 | 'step-backward' => 'f048', 152 | 'fast-backward' => 'f049', 153 | 'backward' => 'f04a', 154 | 'play' => 'f04b', 155 | 'pause' => 'f04c', 156 | 'stop' => 'f04d', 157 | 'forward' => 'f04e', 158 | 'fast-forward' => 'f050', 159 | 'step-forward' => 'f051', 160 | 'eject' => 'f052', 161 | 'chevron-left' => 'f053', 162 | 'chevron-right' => 'f054', 163 | 'plus-sign' => 'f055', 164 | 'minus-sign' => 'f056', 165 | 'remove-sign' => 'f057', 166 | 'ok-sign' => 'f058', 167 | 'question-sign' => 'f059', 168 | 'info-sign' => 'f05a', 169 | 'screenshot' => 'f05b', 170 | 'remove-circle' => 'f05c', 171 | 'ok-circle' => 'f05d', 172 | 'ban-circle' => 'f05e', 173 | 'arrow-left' => 'f060', 174 | 'arrow-right' => 'f061', 175 | 'arrow-up' => 'f062', 176 | 'arrow-down' => 'f063', 177 | 'share-alt' => 'f064', 178 | 'resize-full' => 'f065', 179 | 'resize-small' => 'f066', 180 | 'plus' => 'f067', 181 | 'minus' => 'f068', 182 | 'asterisk' => 'f069', 183 | 'exclamation-sign' => 'f06a', 184 | 'gift' => 'f06b', 185 | 'leaf' => 'f06c', 186 | 'fire' => 'f06d', 187 | 'eye-open' => 'f06e', 188 | 'eye-close' => 'f070', 189 | 'warning-sign' => 'f071', 190 | 'plane' => 'f072', 191 | 'calendar' => 'f073', 192 | 'random' => 'f074', 193 | 'comment' => 'f075', 194 | 'magnet' => 'f076', 195 | 'chevron-up' => 'f077', 196 | 'chevron-down' => 'f078', 197 | 'retweet' => 'f079', 198 | 'shopping-cart' => 'f07a', 199 | 'folder-close' => 'f07b', 200 | 'folder-open' => 'f07c', 201 | 'resize-vertical' => 'f07d', 202 | 'resize-horizontal' => 'f07e', 203 | 'bar-chart' => 'f080', 204 | 'twitter-sign' => 'f081', 205 | 'facebook-sign' => 'f082', 206 | 'camera-retro' => 'f083', 207 | 'key' => 'f084', 208 | 'cogs' => 'f085', 209 | 'comments' => 'f086', 210 | 'thumbs-up' => 'f087', 211 | 'thumbs-down' => 'f088', 212 | 'star-half' => 'f089', 213 | 'heart-empty' => 'f08a', 214 | 'signout' => 'f08b', 215 | 'linkedin-sign' => 'f08c', 216 | 'pushpin' => 'f08d', 217 | 'external-link' => 'f08e', 218 | 'signin' => 'f090', 219 | 'trophy' => 'f091', 220 | 'github-sign' => 'f092', 221 | 'upload-alt' => 'f093', 222 | 'lemon' => 'f094' 223 | } 224 | 225 | char = map[name.to_s] || 'f06a' 226 | 227 | "&#x#{char};" 228 | end 229 | 230 | def link_to(title, target = "", options = {}) 231 | options[:href] = target.respond_to?(:to_url) ? target.to_url : target 232 | options[:data] ||= {} 233 | [:method, :confirm].each { |a| options[:data][a] = options.delete(a) } 234 | haml "%a#{options} #{title}" 235 | end 236 | 237 | def link_to_delete_post(title, post) 238 | link_to title, "/admin/edit/#{post.id}", :method => :delete, :confirm => "Are you sure? This can not be undone." 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/schnitzelpress/markdown_renderer.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | class MarkdownRenderer < Redcarpet::Render::HTML 3 | include Redcarpet::Render::SmartyPants 4 | 5 | def block_code(code, language) 6 | CodeRay.highlight(code, language) 7 | end 8 | 9 | def image(link, title, alt_text) 10 | oembed = OEmbed::Providers.get(link) 11 | %q(
%s
) % [oembed.type, oembed.provider_name.parameterize, oembed.html] 12 | rescue OEmbed::NotFound 13 | %q(%s) % [link, escape_html(title), escape_html(alt_text)] 14 | end 15 | 16 | def escape_html(html) 17 | Rack::Utils.escape_html(html) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/schnitzelpress/post.rb: -------------------------------------------------------------------------------- 1 | require 'tilt' 2 | require 'coderay' 3 | require 'oembed' 4 | 5 | OEmbed::Providers.register_all 6 | SoundCloudProvider = OEmbed::Provider.new("http://soundcloud.com/oembed", :json) 7 | SoundCloudProvider << "http://*.soundcloud.com/*" 8 | OEmbed::Providers.register(SoundCloudProvider) 9 | 10 | module Schnitzelpress 11 | class Post 12 | include Mongoid::Document 13 | include Mongoid::Timestamps 14 | store_in :posts 15 | 16 | # basic data 17 | field :title, :type => String 18 | field :body, :type => String 19 | field :slugs, :type => Array, :default => [] 20 | 21 | # optional fields 22 | field :summary, :type => String 23 | field :link, :type => String 24 | field :read_more, :type => String 25 | 26 | # times & status 27 | field :published_at, :type => DateTime 28 | field :status, :type => Symbol, :default => :draft 29 | 30 | # flags 31 | field :disqus, :type => Boolean, :default => false 32 | 33 | # extra 34 | field :body_html, :type => String 35 | 36 | # indices 37 | index :slugs 38 | index :published_at 39 | index :status 40 | 41 | # validations 42 | validates_presence_of :status, :slug 43 | validates_inclusion_of :status, :in => [:draft, :published] 44 | 45 | scope :published, where(:status => :published) 46 | scope :drafts, where(:status => :draft) 47 | scope :pages, where(:published_at => nil) 48 | scope :posts, where(:published_at.ne => nil) 49 | scope :article_posts, lambda { posts.where(:link => nil) } 50 | scope :link_posts, lambda { posts.where(:link.ne => nil) } 51 | scope :for_year, lambda { |year| d = Date.new(year) ; where(:published_at => (d.beginning_of_year)..(d.end_of_year)) } 52 | scope :for_month, lambda { |year, month| d = Date.new(year, month) ; where(:published_at => (d.beginning_of_month)..(d.end_of_month)) } 53 | scope :for_day, lambda { |year, month, day| d = Date.new(year, month, day) ; where(:published_at => (d.beginning_of_day)..(d.end_of_day)) } 54 | scope :latest, lambda { published.posts.desc(:published_at) } 55 | 56 | before_validation :nil_if_blank 57 | before_validation :set_defaults 58 | validate :validate_slug 59 | before_save :update_body_html 60 | 61 | def disqus_identifier 62 | "post-#{id}" 63 | end 64 | 65 | def slug 66 | slugs.try(:last) 67 | end 68 | 69 | def previous_slugs 70 | slugs[0..-2] 71 | end 72 | 73 | def published_at=(v) 74 | v = Chronic.parse(v) if v.is_a?(String) 75 | super(v) 76 | end 77 | 78 | def slug=(v) 79 | unless v.blank? 80 | slugs.delete(v) 81 | slugs << v 82 | end 83 | end 84 | 85 | def set_defaults 86 | if slug.blank? 87 | self.slug = (title || body.truncate(40, :separator => ' ')).parameterize 88 | end 89 | end 90 | 91 | def validate_slug 92 | conflicting_posts = Post.where(:slugs => slug) 93 | if published_at.present? 94 | conflicting_posts = conflicting_posts.for_day(published_at.year, published_at.month, published_at.day) 95 | end 96 | 97 | if conflicting_posts.any? && conflicting_posts.first != self 98 | errors[:slug] = "This slug is already in use by another post." 99 | end 100 | end 101 | 102 | def nil_if_blank 103 | attributes.keys.each do |attr| 104 | self[attr].strip! if self[attr].is_a?(String) 105 | self[attr] = nil if self[attr] == "" 106 | end 107 | end 108 | 109 | def update_body_html 110 | self.body_html = render 111 | end 112 | 113 | def to_html 114 | if body_html.nil? 115 | update_body_html 116 | save 117 | end 118 | 119 | body_html 120 | end 121 | 122 | def render 123 | @@markdown ||= Redcarpet::Markdown.new(MarkdownRenderer, 124 | :autolink => true, :space_after_headers => true, :fenced_code_blocks => true) 125 | 126 | @@markdown.render(body.to_s) 127 | end 128 | 129 | def post? 130 | published_at.present? 131 | end 132 | 133 | def page? 134 | !post? 135 | end 136 | 137 | def published? 138 | status == :published 139 | end 140 | 141 | def draft? 142 | status == :draft 143 | end 144 | 145 | def link_post? 146 | link.present? 147 | end 148 | 149 | def article_post? 150 | link.nil? 151 | end 152 | 153 | def year 154 | published_at.year 155 | end 156 | 157 | def month 158 | published_at.month 159 | end 160 | 161 | def day 162 | published_at.day 163 | end 164 | 165 | def home_page? 166 | slug == 'home' 167 | end 168 | 169 | def to_url 170 | if home_page? 171 | '/' 172 | else 173 | published_at.present? ? "/#{sprintf '%04d', year}/#{sprintf '%02d', month}/#{sprintf '%02d', day}/#{slug}/" : "/#{slug}/" 174 | end 175 | end 176 | 177 | def disqus? 178 | disqus && published? 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/schnitzelpress/static.rb: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /lib/schnitzelpress/version.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/templates/new_blog/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .rbfu-version 3 | .powenv 4 | .sass-cache 5 | tmp 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /lib/templates/new_blog/Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'unicorn', '~> 4.2.0' 4 | gem 'schnitzelpress', :git => 'git://github.com/hmans/schnitzelpress.git' 5 | 6 | group :development do 7 | gem 'shotgun' 8 | gem 'heroku' 9 | end 10 | -------------------------------------------------------------------------------- /lib/templates/new_blog/Gemfile.lock.tt: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/hmans/schnitzelpress.git 3 | revision: 979750e4f9b8a53b6a751b6e999ab46348b9e02e 4 | specs: 5 | schnitzelpress (0.2.0) 6 | activesupport (~> 3.2.0) 7 | bson_ext (~> 1.5.0) 8 | chronic (~> 0.6.7) 9 | coderay (~> 1.0.5) 10 | haml (~> 3.1.4) 11 | i18n (~> 0.6.0) 12 | mongo (~> 1.5.2) 13 | mongoid (~> 2.4.0) 14 | omniauth (~> 1.0.2) 15 | omniauth-browserid (~> 0.0.1) 16 | packr (~> 3.1.1) 17 | pry (~> 0.9.8) 18 | rack (~> 1.4.1) 19 | rack-cache (~> 1.1.0) 20 | rack-contrib (~> 1.1.0) 21 | rake (~> 0.9.2.2) 22 | redcarpet (~> 2.1.0) 23 | ruby-oembed (~> 0.8.5) 24 | sass (~> 3.1.15) 25 | schnitzelstyle (~> 0.1.1) 26 | sinatra (~> 1.3.2) 27 | sinatra-contrib (~> 1.3.1) 28 | thor (~> 0.14.6) 29 | tilt (~> 1.3.0) 30 | 31 | GEM 32 | remote: http://rubygems.org/ 33 | specs: 34 | activemodel (3.2.3) 35 | activesupport (= 3.2.3) 36 | builder (~> 3.0.0) 37 | activesupport (3.2.3) 38 | i18n (~> 0.6) 39 | multi_json (~> 1.0) 40 | addressable (2.2.7) 41 | backports (2.5.1) 42 | bson (1.5.2) 43 | bson_ext (1.5.2) 44 | bson (= 1.5.2) 45 | builder (3.0.0) 46 | chronic (0.6.7) 47 | coderay (1.0.5) 48 | eventmachine (0.12.10) 49 | faraday (0.7.6) 50 | addressable (~> 2.2) 51 | multipart-post (~> 1.1) 52 | rack (~> 1.1) 53 | haml (3.1.4) 54 | hashie (1.2.0) 55 | heroku (2.23.0) 56 | launchy (>= 0.3.2) 57 | netrc (~> 0.7.1) 58 | rest-client (~> 1.6.1) 59 | rubyzip 60 | i18n (0.6.0) 61 | kgio (2.7.4) 62 | launchy (2.1.0) 63 | addressable (~> 2.2.6) 64 | method_source (0.7.1) 65 | mime-types (1.18) 66 | mongo (1.5.2) 67 | bson (= 1.5.2) 68 | mongoid (2.4.7) 69 | activemodel (~> 3.1) 70 | mongo (~> 1.3) 71 | tzinfo (~> 0.3.22) 72 | multi_json (1.2.0) 73 | multipart-post (1.1.5) 74 | netrc (0.7.1) 75 | omniauth (1.0.3) 76 | hashie (~> 1.2) 77 | rack 78 | omniauth-browserid (0.0.1) 79 | faraday 80 | multi_json 81 | omniauth (~> 1.0) 82 | oyster (0.9.5) 83 | packr (3.1.1) 84 | oyster (>= 0.9.5) 85 | pry (0.9.8.4) 86 | coderay (~> 1.0.5) 87 | method_source (~> 0.7.1) 88 | slop (>= 2.4.4, < 3) 89 | rack (1.4.1) 90 | rack-cache (1.1) 91 | rack (>= 0.4) 92 | rack-contrib (1.1.0) 93 | rack (>= 0.9.1) 94 | rack-protection (1.2.0) 95 | rack 96 | rack-test (0.6.1) 97 | rack (>= 1.0) 98 | raindrops (0.8.0) 99 | rake (0.9.2.2) 100 | redcarpet (2.1.1) 101 | rest-client (1.6.7) 102 | mime-types (>= 1.16) 103 | ruby-oembed (0.8.7) 104 | rubyzip (0.9.6.1) 105 | sass (3.1.15) 106 | schnitzelstyle (0.1.2) 107 | sass 108 | shotgun (0.9) 109 | rack (>= 1.0) 110 | sinatra (1.3.2) 111 | rack (~> 1.3, >= 1.3.6) 112 | rack-protection (~> 1.2) 113 | tilt (~> 1.3, >= 1.3.3) 114 | sinatra-contrib (1.3.1) 115 | backports (>= 2.0) 116 | eventmachine 117 | rack-protection 118 | rack-test 119 | sinatra (~> 1.3.0) 120 | tilt (~> 1.3) 121 | slop (2.4.4) 122 | thor (0.14.6) 123 | tilt (1.3.3) 124 | tzinfo (0.3.32) 125 | unicorn (4.2.1) 126 | kgio (~> 2.6) 127 | rack 128 | raindrops (~> 0.7) 129 | 130 | PLATFORMS 131 | ruby 132 | 133 | DEPENDENCIES 134 | heroku 135 | schnitzelpress! 136 | shotgun 137 | unicorn (~> 4.2.0) 138 | -------------------------------------------------------------------------------- /lib/templates/new_blog/Procfile: -------------------------------------------------------------------------------- 1 | web: echo "worker_processes 4 ; timeout 30" | bundle exec unicorn -p $PORT -c /dev/stdin 2 | -------------------------------------------------------------------------------- /lib/templates/new_blog/config.ru.tt: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'bundler' 4 | Bundler.require 5 | 6 | $stdout.sync = true 7 | run Schnitzelpress.omnomnom! 8 | -------------------------------------------------------------------------------- /lib/views/404.haml: -------------------------------------------------------------------------------- 1 | %section 2 | %h1 400 + 4 3 | %p 4 | The document you requested could not be found. 5 | -------------------------------------------------------------------------------- /lib/views/admin/admin.haml: -------------------------------------------------------------------------------- 1 | %section 2 | %h1 #{icon 'cog'} Administration 3 | %p 4 | You're logged in as #{session[:auth][:uid]}. 5 | %ul.admin 6 | %li 7 | %a.green.button{:href => '/admin/new'} Create new Post or Page 8 | %a.blue.button{:href => '/admin/config'} Configuration 9 | %a.red.button{:href => '/logout'} Logout 10 | 11 | = partial "admin_post_list", :posts => @drafts, :title => "Drafts" 12 | = partial "admin_post_list", :posts => @posts, :title => "Published Posts" 13 | = partial "admin_post_list", :posts => @pages, :title => "Pages" 14 | -------------------------------------------------------------------------------- /lib/views/admin/config.haml: -------------------------------------------------------------------------------- 1 | - set_page_title "Configuration" 2 | 3 | %section 4 | %form.post{:action => '/admin/config', :method => 'post'} 5 | %h2 Blog 6 | = form_field config, :blog_title 7 | = form_field config, :blog_description 8 | = form_field config, :blog_footer 9 | 10 | %h2 Author 11 | = form_field config, :author_name 12 | 13 | %h2 External Services 14 | = form_field config, :blog_feed_url, :label => "ATOM Feed URL", :hint => "This is what the /feed URL will redirect to. You can change this to eg. your FeedBurner URL." 15 | .row 16 | .six.columns 17 | = form_field config, :disqus_id, :label => "Disqus shortname" 18 | .six.columns 19 | = form_field config, :twitter_id, :label => "Twitter user name" 20 | .row 21 | .six.columns 22 | = form_field config, :google_analytics_id, :label => "Google Analytics ID" 23 | .six.columns 24 | = form_field config, :gauges_id, :label => "Gauges ID" 25 | .row 26 | .six.columns 27 | = form_field config, :gosquared_id, :label => "GoSquared ID" 28 | 29 | .buttons 30 | %input{:type => 'submit', :value => 'Update Configuration'} 31 | -------------------------------------------------------------------------------- /lib/views/admin/edit.haml: -------------------------------------------------------------------------------- 1 | %section 2 | = partial 'post_form', :post => @post 3 | -------------------------------------------------------------------------------- /lib/views/admin/new.haml: -------------------------------------------------------------------------------- 1 | %section 2 | = partial 'post_form', :post => @post 3 | -------------------------------------------------------------------------------- /lib/views/atom.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %feed{:xmlns => 'http://www.w3.org/2005/Atom'} 3 | %title= config.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= config.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= config.author_name 21 | %content{:type => 'html'} 22 | :cdata 23 | #{post.to_html} 24 | -------------------------------------------------------------------------------- /lib/views/blog.scss: -------------------------------------------------------------------------------- 1 | @import 'schnitzelpress'; 2 | -------------------------------------------------------------------------------- /lib/views/index.haml: -------------------------------------------------------------------------------- 1 | %section.posts 2 | - if @posts.any? 3 | - @posts.each do |post| 4 | = partial post 5 | - else 6 | #welcome 7 | :redcarpet 8 | **Congratulations on setting up your new Schnitzelpress blog!** 9 | A world of hackery and adventure awaits. 10 | 11 | 12 | To get started, please log into your blog's 13 | [Administration Section](/admin). 14 | 15 | Visit www.schnitzelpress.org for the latest news and gossip! 16 | 17 | %footer 18 | - if @show_previous_posts_button 19 | %a.button{:href => "/?page=#{params[:page].to_i + 1}"} View Older Posts 20 | -------------------------------------------------------------------------------- /lib/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %title= [@page_title, config.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 => "/assets/schnitzelpress.#{ASSET_TIMESTAMP}.css", :media => "screen", :rel => "stylesheet", :type => "text/css" } 8 | %link{ :href => config.blog_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 => '/'}= h config.blog_title 14 | - if @show_description 15 | ~ markdown config.blog_description 16 | 17 | #actions.admin_only 18 | = yield_content :actions 19 | %a{:href => '/admin/'} #{icon 'cog'} Go To Admin 20 | %a{:href => '/logout'} #{icon 'signout'} Logout 21 | 22 | = yield 23 | 24 | %footer 25 | ~ markdown config.blog_footer 26 | 27 | - if production? && config.google_analytics_id.present? 28 | = partial 'google_analytics' 29 | - if production? && config.gauges_id.present? 30 | = partial 'gauges' 31 | - if production? && config.gosquared_id.present? 32 | = partial 'gosquared' 33 | 34 | %script{ :type => 'text/javascript', :src => "/assets/schnitzelpress.#{ASSET_TIMESTAMP}.js" } 35 | -------------------------------------------------------------------------------- /lib/views/login.haml: -------------------------------------------------------------------------------- 1 | - @page_title = "Login" 2 | 3 | %section 4 | %p 5 | Please use one of the following services to log in: 6 | 7 | %p 8 | %a.button#browser_id{:href => '/auth/browser_id'} Log in with Mozilla Persona 9 | - if Schnitzelpress.env.development? 10 | %a.green.button{:href => '/auth/developer'} Developer Login 11 | 12 | %form{:method => 'post', :action => '/auth/browser_id/callback', :noValidate => 'noValidate'} 13 | %input{:type => 'hidden', :name => 'assertion'} 14 | 15 | %script{ :type => 'text/javascript', :src => '//browserid.org/include.js' } 16 | -------------------------------------------------------------------------------- /lib/views/partials/_admin_post_list.haml: -------------------------------------------------------------------------------- 1 | - if posts.any? 2 | %h2= title 3 | .admin-post-list 4 | - posts.each do |post| 5 | .row 6 | .ten.columns 7 | %a{:href => "/admin/edit/#{post.id}"}= h (post.title || ("%s..." % post.body.first(50))) 8 | .two.columns.icons 9 | %a{:href => url_for(post)}= icon 'eye-open' 10 | %a{:href => "/admin/edit/#{post.id}"}= icon 'edit' 11 | = link_to_delete_post icon('trash'), post 12 | -------------------------------------------------------------------------------- /lib/views/partials/_disqus.haml: -------------------------------------------------------------------------------- 1 | #disqus_thread 2 | 3 | :javascript 4 | /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */ 5 | var disqus_shortname = '#{config.disqus_id}'; 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 comments powered by Disqus. 18 | -------------------------------------------------------------------------------- /lib/views/partials/_form_field.haml: -------------------------------------------------------------------------------- 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 => options[: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 | - when :boolean 23 | %input{field_options.merge(:type => 'hidden', :value => "0")} 24 | %input{field_options.merge(:type => 'checkbox', :value => "1", :checked => options[:value])} 25 | %span.value Enable 26 | 27 | - else # normal inputs 28 | %input{field_options.merge(:value => options[:value])} 29 | 30 | - if options[:errors] 31 | .error= options[:errors].join(", ") 32 | 33 | - if options[:hint] 34 | .hint= options[:hint] 35 | -------------------------------------------------------------------------------- /lib/views/partials/_gauges.haml: -------------------------------------------------------------------------------- 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', '#{config.gauges_id}'); 9 | t.src = '//secure.gaug.es/track.js'; 10 | var s = document.getElementsByTagName('script')[0]; 11 | s.parentNode.insertBefore(t, s); 12 | })(); 13 | -------------------------------------------------------------------------------- /lib/views/partials/_google_analytics.haml: -------------------------------------------------------------------------------- 1 | :javascript 2 | var _gaq = _gaq || []; 3 | _gaq.push(['_setAccount', '#{config.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 | })(); 11 | -------------------------------------------------------------------------------- /lib/views/partials/_gosquared.haml: -------------------------------------------------------------------------------- 1 | :javascript 2 | var GoSquared = {}; 3 | GoSquared.acct = '#{config.gosquared_id}'; 4 | (function(w){ 5 | function gs(){ 6 | w._gstc_lt = +new Date; 7 | var d = document, g = d.createElement("script"); 8 | g.type = "text/javascript"; 9 | g.src = "//d1l6p2sc9645hc.cloudfront.net/tracker.js"; 10 | var s = d.getElementsByTagName("script")[0]; 11 | s.parentNode.insertBefore(g, s); 12 | } 13 | w.addEventListener ? 14 | w.addEventListener("load", gs, false) : 15 | w.attachEvent("onload", gs); 16 | })(window); 17 | -------------------------------------------------------------------------------- /lib/views/partials/_post.haml: -------------------------------------------------------------------------------- 1 | - complete ||= false 2 | - show_title ||= post.title.present? 3 | - show_summary ||= post.summary.present? 4 | - show_body ||= complete || !show_summary 5 | - show_read_more ||= !complete && post.summary.present? 6 | - show_permalink ||= true 7 | - show_twitter ||= complete && post.post? && config.twitter_id.present? 8 | 9 | %article.post{:class => [post.status, post.link_post? ? 'link' : 'article']} 10 | %header 11 | - if show_title 12 | %h1 13 | %a.instapaper_title{:href => post.link || post.to_url}= h post.title 14 | - if post.link_post? 15 | %span.link-arrow ➝ 16 | 17 | - if show_summary 18 | .summary 19 | ~ markdown post.summary 20 | - if show_read_more 21 | %p 22 | %a{:href => url_for(post)}= post.read_more.presence || "Read Complete Article" 23 | → 24 | 25 | - if show_body 26 | .instapaper_body 27 | ~ post.to_html 28 | 29 | %footer 30 | - if show_permalink 31 | %p.permalink 32 | %a{:href => url_for(post)}= post.published_at.try(:to_date) || "∞" 33 | 34 | - if show_twitter 35 | .social_media_buttons 36 | - if show_twitter 37 | %a{:href => "https://twitter.com/share", :class => "twitter-share-button", :data => {:via => config.twitter_id}} 38 | :javascript 39 | !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"); 40 | -------------------------------------------------------------------------------- /lib/views/partials/_post_form.haml: -------------------------------------------------------------------------------- 1 | %form.post{:action => @post.new_record? ? '/admin/new' : "/admin/edit/#{@post.id}", :method => 'post'} 2 | %input{:type => 'hidden', :name => '_method', :value => @post.new_record? ? 'post' : 'put'} 3 | = form_field @post, :title, :placeholder => "Title of your post." 4 | = form_field @post, :link, :placeholder => "Optional link to external URL." 5 | = form_field @post, :summary, :type => :textarea, :rows => 5, :placeholder => "An optional summary of your post. Markdown formatting available." 6 | = form_field @post, :body, :type => :textarea, :rows => 20, :placeholder => "Your post's body of text. Markdown formatting available." 7 | = 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." 8 | 9 | .row 10 | .four.columns 11 | = 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 12 | .four.columns 13 | = form_field @post, :published_at, :placeholder => 'Try "now", "in 3 days", ...' 14 | .two.columns 15 | = form_field @post, :status, :type => :dropdown, :options => [[:draft, "Draft"], [:published, "Published"]] 16 | .two.columns 17 | = form_field @post, :disqus 18 | 19 | .buttons 20 | %button.green{:type => 'submit'} #{icon 'ok'} #{@post.new_record? ? 'Create Post' : 'Update Post'} 21 | or 22 | = link_to_delete_post "delete this post", @post 23 | -------------------------------------------------------------------------------- /lib/views/post.haml: -------------------------------------------------------------------------------- 1 | - set_page_title @post.title.presence 2 | 3 | = partial @post, :complete => true 4 | 5 | - if @extra_posts 6 | %section.extra_posts 7 | %h1 8 | %a{:href => '/blog'}= @extra_posts.first 9 | %ul 10 | - for post in @extra_posts.second 11 | %li 12 | %a{:href => post.to_url}= h post.title 13 | 14 | - if show_disqus? && @post.disqus? 15 | %section.disqus 16 | = partial 'disqus', :disqus_identifier => @post.disqus_identifier 17 | 18 | - content_for :actions do 19 | %a{:href => "/admin/edit/%s" % @post.id} #{icon 'edit'} Edit 20 | -------------------------------------------------------------------------------- /lib/views/schnitzelpress.scss: -------------------------------------------------------------------------------- 1 | // $font-1: 16px "Palatino","Georgia",serif !default; 2 | // $font-2: 14px "Lucida Grande","Tahoma",sans-serif !default; 3 | 4 | @import 'schnitzelstyle/complete'; 5 | 6 | /* background image */ 7 | body, .container { 8 | background-image: url(/img/background.png); 9 | } 10 | 11 | .admin_only { 12 | display: none; 13 | body.show_admin & { display: block; } 14 | } 15 | 16 | /* action panel */ 17 | #actions { 18 | @include small-type; 19 | margin: 5px; 20 | 21 | a { 22 | display: inline-block; 23 | padding: 3px 6px; 24 | background-color: $color-text; 25 | color: $color-background; 26 | font-weight: bold; 27 | text-decoration: none; 28 | border-radius: 3px; 29 | @include appear-on-hover; 30 | } 31 | 32 | @media only screen and (min-width: 641px) { 33 | position: fixed; 34 | top: 0px; 35 | right: 0px; 36 | } 37 | } 38 | 39 | /* posts */ 40 | article.post { 41 | header { 42 | h1 { 43 | span.link-arrow { 44 | color: lighten($color-link, 30%); 45 | } 46 | } 47 | } 48 | 49 | div.summary { 50 | p:last-child a { 51 | font-weight: bold; 52 | } 53 | } 54 | 55 | footer { 56 | margin-top: 0.5em; 57 | 58 | .social_media_buttons { 59 | @include animated; 60 | 61 | margin: 2em 0; 62 | @include appear-on-hover; 63 | } 64 | } 65 | } 66 | 67 | // admin 68 | ul.admin { 69 | list-style: none; 70 | 71 | li { 72 | display: inline; 73 | margin-left: 0 !important; 74 | margin-right: 5px; 75 | 76 | a { 77 | @include small-type; 78 | } 79 | } 80 | } 81 | 82 | div.admin-post-list { 83 | padding: 5px 0; 84 | margin: 1em 0; 85 | max-height: 300px; 86 | overflow: auto; 87 | border-bottom: 1px dotted rgba($color-text, 0.5); 88 | border-top: 1px dotted rgba($color-text, 0.5); 89 | 90 | .icons { 91 | display: none; 92 | text-align: right; 93 | a { 94 | color: $color-footer; 95 | &:hover { color: $color-text }; 96 | } 97 | } 98 | .row { 99 | padding: 3px; 100 | margin-bottom: 0; 101 | &:hover { 102 | background-color: rgba($color-text, 0.05); 103 | .icons { 104 | display: block; 105 | } 106 | } 107 | } 108 | a { 109 | border: none; 110 | } 111 | } 112 | 113 | // forms 114 | .input.post_title input { @include large-type; } 115 | 116 | // welcome message 117 | section.posts #welcome { 118 | font: $font-header; 119 | font-size: 1.8em; 120 | color: rgba($color-text, 0.5); 121 | p { text-align: left; line-height: 1.3; } 122 | strong { color: $color-text; } 123 | p:first-child { font-size: 130%; } 124 | span.heart { color: #c66; } 125 | } 126 | 127 | // disqus 128 | #dsq-content { 129 | a { 130 | border: 0; 131 | } 132 | #dsq-reply { 133 | margin-bottom: 2em; 134 | } 135 | h3 { 136 | margin-top: 1em !important; 137 | @include clearfix; 138 | } 139 | } 140 | 141 | // font-awesome 142 | @font-face { 143 | font-family: 'FontAwesome'; 144 | src: url('/font/fontawesome-webfont.eot'); 145 | src: url('/font/fontawesome-webfont.eot?#iefix') format('embedded-opentype'), url('/font/fontawesome-webfont.woff') format('woff'), url('/font/fontawesome-webfont.ttf') format('truetype'), url('/font/fontawesome-webfont.svgz#FontAwesomeRegular') format('svg'), url('/font/fontawesome-webfont.svg#FontAwesomeRegular') format('svg'); 146 | font-weight: normal; 147 | font-style: normal; 148 | } 149 | .font-awesome { 150 | font-family: FontAwesome; 151 | font-weight: normal; 152 | font-style: normal; 153 | } 154 | -------------------------------------------------------------------------------- /schnitzelpress.gemspec: -------------------------------------------------------------------------------- 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 lean, mean blogging machine for hackers and fools.} 8 | gem.summary = %q{A lean, mean blogging machine for hackers and fools.} 9 | gem.homepage = "http://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.1' 20 | gem.add_dependency 'rack-contrib', '~> 1.1.0' 21 | gem.add_dependency 'rack-cache', '~> 1.1.0' 22 | gem.add_dependency 'sinatra', '~> 1.3.2' 23 | gem.add_dependency 'sinatra-contrib', '~> 1.3.1' 24 | gem.add_dependency 'activesupport', '~> 3.2.0' 25 | 26 | # database related 27 | gem.add_dependency 'mongo', '~> 1.5.2' 28 | gem.add_dependency 'mongoid', '~> 2.4.0' 29 | gem.add_dependency 'bson_ext', '~> 1.5.0' 30 | 31 | # authentication 32 | gem.add_dependency 'omniauth', '~> 1.0.2' 33 | gem.add_dependency 'omniauth-browserid', '~> 0.0.1' 34 | 35 | # frontend/views/assets related 36 | gem.add_dependency 'haml', '~> 3.1.4' 37 | # No need to require Sass here, since we're already requiring Schnitzelstyle. 38 | # gem.add_dependency 'sass', '~> 3.1.15' 39 | gem.add_dependency 'redcarpet', '~> 2.1.0' 40 | gem.add_dependency 'coderay', '~> 1.0.5' 41 | gem.add_dependency 'schnitzelstyle', '~> 0.1.1' 42 | gem.add_dependency 'i18n', '~> 0.6.0' 43 | gem.add_dependency 'tilt', '~> 1.3.0' 44 | gem.add_dependency 'ruby-oembed', '~> 0.8.5' 45 | gem.add_dependency 'packr', '~> 3.1.1' 46 | 47 | # CLI related 48 | gem.add_dependency 'thor', '~> 0.14.6' 49 | gem.add_dependency 'rake', '~> 0.9.2.2' 50 | gem.add_dependency 'pry', '~> 0.9.8' 51 | 52 | # misc 53 | gem.add_dependency 'chronic', '~> 0.6.7' 54 | 55 | # development dependencies 56 | gem.add_development_dependency 'rspec', '>= 2.8.0' 57 | gem.add_development_dependency 'rspec-html-matchers' 58 | gem.add_development_dependency 'database_cleaner' 59 | gem.add_development_dependency 'factory_girl', '~> 2.6.0' 60 | gem.add_development_dependency 'ffaker' 61 | gem.add_development_dependency 'timecop' 62 | gem.add_development_dependency 'shotgun' 63 | gem.add_development_dependency 'rack-test' 64 | gem.add_development_dependency 'watchr' 65 | gem.add_development_dependency 'awesome_print' 66 | end 67 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Schnitzelpress::App do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | Schnitzelpress::App 8 | end 9 | 10 | describe 'the home page' do 11 | before do 12 | 2.times { Factory(:draft_post) } 13 | 5.times { Factory(:published_post) } 14 | get '/' 15 | end 16 | 17 | subject { last_response } 18 | 19 | it { should be_ok } 20 | its(:body) { should have_tag 'title', :text => Schnitzelpress::Config.instance.blog_title } 21 | its(:body) { should have_tag 'section.posts > article.post.published', :count => 5 } 22 | its(:body) { should_not have_tag 'section.posts > article.post.draft' } 23 | end 24 | 25 | describe 'the /blog page' do 26 | before { get '/blog' } 27 | subject { last_response } 28 | it { should be_ok } 29 | end 30 | 31 | describe 'the public feed url' do 32 | before do 33 | Schnitzelpress::Config.instance.blog_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 == 307 } 40 | specify { subject["Location"].should == 'http://example.org/blog.atom' } 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(:published_post, :published_at => "2011-12-10 12:00", :slugs => ['ancient-slug', 'old-slug', 'current-slug']) 47 | end 48 | 49 | it 'should enforce the canonical URL' do 50 | get "/2011/12/10/ancient-slug/" 51 | last_response.should be_redirect 52 | last_response["Location"].should == "http://example.org/2011/12/10/current-slug/" 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/assets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Schnitzelpress::Actions::Assets' do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | Schnitzelpress::App 8 | end 9 | 10 | describe '/assets/schnitzelpress.*.js' do 11 | before do 12 | Schnitzelpress::JavascriptPacker.should_receive(:pack_javascripts!).and_return('{123}') 13 | get '/assets/schnitzelpress.123.js' 14 | end 15 | subject { last_response } 16 | it { should be_ok } 17 | its(:body) { should == '{123}' } 18 | end 19 | 20 | describe '/assets/schnitzelpress.*.css' do 21 | before { get '/assets/schnitzelpress.123.css' } 22 | subject { last_response } 23 | it { should be_ok } 24 | its(:content_type) { should == 'text/css;charset=utf-8' } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :post, :class => Schnitzelpress::Post do 3 | title { Faker::Lorem.sentence } 4 | body { Faker::Lorem.paragraphs } 5 | published_at { Time.now } 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 29 | -------------------------------------------------------------------------------- /spec/post_spec.rb: -------------------------------------------------------------------------------- 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 { subject.slug = nil } 21 | 22 | context "when a title is available" do 23 | before { subject.title = "Team Schnitzel is AWESOME!" } 24 | 25 | it "should set its slug to a sluggified version of its title" do 26 | expect { subject.save }.to change(subject, :slug). 27 | from(nil). 28 | to('team-schnitzel-is-awesome') 29 | end 30 | end 31 | 32 | context "when no title is available" do 33 | before do 34 | subject.title = nil 35 | subject.body = "Team Schnitzel is AWESOME! Lorem ipsum and so on." 36 | end 37 | 38 | it "should set its slug to a sluggified version of the truncated body" do 39 | expect { subject.save }.to change(subject, :slug). 40 | from(nil). 41 | to('team-schnitzel-is-awesome-lorem') 42 | end 43 | end 44 | end 45 | 46 | context "when another post on the same day is already using the same slug" do 47 | before do 48 | @other_post = Factory(:published_post, :slugs => ["amazing-slug"]) 49 | subject.published_at = @other_post.published_at 50 | subject.slug = "amazing-slug" 51 | end 52 | 53 | it { should_not be_valid } 54 | end 55 | 56 | context "when another page is using the same slug" do 57 | subject { Factory.build(:draft_page) } 58 | 59 | before do 60 | @other_page = Factory(:published_page, :slugs => ["amazing-slug"]) 61 | subject.slug = "amazing-slug" 62 | end 63 | 64 | it { should_not be_valid } 65 | end 66 | 67 | it "should store blank attributes as nil" do 68 | subject.link = "" 69 | expect { subject.save }.to change(subject, :link).from("").to(nil) 70 | end 71 | 72 | it "should remove leading and trailing spaces from string attributes" do 73 | subject.link = " moo " 74 | subject.link.should == " moo " 75 | subject.save 76 | subject.link.should == "moo" 77 | end 78 | end 79 | 80 | describe '.latest' do 81 | it 'should return the latest published posts' do 82 | 2.times { Factory :draft_post } 83 | 5.times { Factory :published_post } 84 | Schnitzelpress::Post.latest.size.should == 5 85 | end 86 | end 87 | 88 | context 'date methods' do 89 | before { subject.published_at = "2012-01-02 12:23:13" } 90 | its(:year) { should == 2012 } 91 | its(:month) { should == 01 } 92 | its(:day) { should == 02 } 93 | end 94 | 95 | context 'to_url' do 96 | it 'should produce double-digit months and days' do 97 | @post = Factory.build(:post, :published_at => '2012-1-1 12:00:00', :slug => 'test') 98 | @post.to_url.should == '/2012/01/01/test/' 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 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 | Schnitzelpress::Config.forget_instance 32 | end 33 | 34 | config.after(:each) do 35 | DatabaseCleaner[:mongoid].clean 36 | end 37 | end 38 | --------------------------------------------------------------------------------