├── tmp
└── .keep
├── sample
├── .gitignore
├── _templates
│ ├── draft
│ ├── page
│ └── post
├── _posts
│ ├── 2015-01-30-test.markdown
│ └── 2015-01-29-welcome-to-jekyll.markdown
├── _layouts
│ ├── page.html
│ ├── default.html
│ └── post.html
├── about.md
├── index.html
├── _includes
│ ├── head.html
│ ├── header.html
│ └── footer.html
├── css
│ └── main.scss
├── feed.xml
└── _sass
│ ├── _syntax-highlighting.scss
│ ├── _base.scss
│ └── _layout.scss
├── .rspec
├── .bowerrc
├── app
├── templates
│ ├── frontend
│ │ └── index.html.erb
│ └── application.html.erb
├── views
│ ├── frontend
│ │ └── index.rb
│ ├── application_layout.rb
│ ├── posts
│ │ ├── upload.rb
│ │ ├── show.rb
│ │ ├── create.rb
│ │ ├── destroy.rb
│ │ ├── restore.rb
│ │ ├── revert.rb
│ │ ├── update.rb
│ │ └── index.rb
│ ├── version
│ │ └── show.rb
│ ├── deploys
│ │ └── create.rb
│ ├── site
│ │ └── show.rb
│ └── syncs
│ │ └── create.rb
├── controllers
│ ├── frontend
│ │ └── index.rb
│ ├── posts
│ │ ├── show.rb
│ │ ├── revert.rb
│ │ ├── destroy.rb
│ │ ├── manage.rb
│ │ ├── restore.rb
│ │ ├── index.rb
│ │ ├── create.rb
│ │ ├── upload.rb
│ │ └── update.rb
│ ├── site
│ │ └── show.rb
│ ├── version
│ │ └── show.rb
│ ├── deploys
│ │ └── create.rb
│ └── syncs
│ │ └── create.rb
├── config
│ ├── environment.rb
│ ├── routes.rb
│ └── sprockets.rb
├── assets
│ ├── javascripts
│ │ ├── app.js.coffee
│ │ └── components
│ │ │ ├── router.js.cjsx
│ │ │ ├── header.js.cjsx
│ │ │ └── posts.js.cjsx
│ └── stylesheets
│ │ └── app.scss
└── octodmin.rb
├── .lotusrc
├── lib
├── octodmin
│ ├── version.rb
│ ├── app.rb
│ ├── site.rb
│ └── post.rb
└── octodmin.rb
├── spec
├── fixtures
│ └── ear.png
├── api
│ ├── version_spec.rb
│ ├── site_spec.rb
│ ├── deploys_spec.rb
│ ├── syncs_spec.rb
│ └── posts_spec.rb
├── frontend
│ ├── home_spec.rb
│ └── assets_spec.rb
└── spec_helper.rb
├── config.ru
├── .travis.yml
├── .gitignore
├── bower.json
├── Gemfile
├── _deploy.yml
├── CHANGELOG.md
├── _config.yml
├── Rakefile
├── octodmin.gemspec
├── LICENSE.md
└── README.md
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format=documentation
3 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory" : "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/app/templates/frontend/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.lotusrc:
--------------------------------------------------------------------------------
1 | architecture=container
2 | test=minitest
3 | template=erb
4 |
--------------------------------------------------------------------------------
/lib/octodmin/version.rb:
--------------------------------------------------------------------------------
1 | module Octodmin
2 | VERSION = "0.3.5"
3 | end
4 |
--------------------------------------------------------------------------------
/sample/_templates/draft:
--------------------------------------------------------------------------------
1 | ---
2 | layout: {{ layout }}
3 | title: {{ title }}
4 | ---
5 |
--------------------------------------------------------------------------------
/sample/_templates/page:
--------------------------------------------------------------------------------
1 | ---
2 | layout: {{ layout }}
3 | title: {{ title }}
4 | ---
5 |
--------------------------------------------------------------------------------
/lib/octodmin/app.rb:
--------------------------------------------------------------------------------
1 | require_relative "../octodmin"
2 | require_relative "../../app/octodmin"
3 |
--------------------------------------------------------------------------------
/spec/fixtures/ear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasnoukhov/octodmin/HEAD/spec/fixtures/ear.png
--------------------------------------------------------------------------------
/app/views/frontend/index.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Frontend
2 | class Index
3 | include Octodmin::View
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/application_layout.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views
2 | class ApplicationLayout
3 | include Octodmin::Layout
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | ENV["RACK_ENV"] = ENV["LOTUS_ENV"] ||= "development"
2 |
3 | require "bundler/setup"
4 | require "octodmin/app"
5 | run Octodmin::App.new(__dir__)
6 |
--------------------------------------------------------------------------------
/sample/_templates/post:
--------------------------------------------------------------------------------
1 | ---
2 | layout: {{ layout }}
3 | title: {{ title }}
4 | date: {{ date }}
5 | custom: default
6 | ---
7 |
8 | New post from **template**
9 |
--------------------------------------------------------------------------------
/lib/octodmin.rb:
--------------------------------------------------------------------------------
1 | require "jekyll"
2 | require "octopress"
3 | require "babosa"
4 |
5 | require "octodmin/version"
6 | require "octodmin/site"
7 | require "octodmin/post"
8 |
--------------------------------------------------------------------------------
/app/controllers/frontend/index.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Frontend
2 | class Index
3 | include Octodmin::Action
4 |
5 | def call(params)
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/sample/_posts/2015-01-30-test.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Test"
4 | date: 2015-01-30 18:10:00
5 | custom: data
6 | ---
7 |
8 | # OMG
9 |
10 | This is a *post*!
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 | before_script:
4 | - npm install -g bower
5 | - bower install
6 | script: bundle exec rake spec:coverage
7 | rvm:
8 | - 2.0
9 | - 2.1
10 | - 2.2
11 |
--------------------------------------------------------------------------------
/app/config/environment.rb:
--------------------------------------------------------------------------------
1 | require "lotus/setup"
2 | ENV["RACK_ENV"] = ENV["LOTUS_ENV"] ||= "development"
3 |
4 | require_relative "../octodmin"
5 | Lotus::Container.configure do
6 | mount Octodmin::App, at: "/"
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/posts/upload.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Upload
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(uploads: [upload])
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/version/show.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Version
2 | class Show
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(versions: version)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/deploys/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Deploys
2 | class Create
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(deploys: [message])
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/show.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Show
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/site/show.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Site
2 | class Show
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(sites: site.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Create
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/destroy.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Destroy
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/restore.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Restore
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/revert.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Revert
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/update.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Update
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: post.serializable_hash)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/posts/index.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Posts
2 | class Index
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(posts: posts.map(&:serializable_hash))
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/syncs/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Views::Syncs
2 | class Create
3 | include Octodmin::View
4 | format :json
5 |
6 | def render
7 | _raw JSON.dump(syncs: [message.gsub("/", "/")])
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/controllers/posts/show.rb:
--------------------------------------------------------------------------------
1 | require_relative "manage"
2 |
3 | module Octodmin::Controllers::Posts
4 | class Show < Manage
5 | include Octodmin::Action
6 | expose :post
7 |
8 | def call(params)
9 | super
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/site/show.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Site
2 | class Show
3 | include Octodmin::Action
4 | expose :site
5 |
6 | def call(params)
7 | self.format = :json
8 |
9 | @site = Octodmin::Site.new
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/version/show.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Version
2 | class Show
3 | include Octodmin::Action
4 | expose :version
5 |
6 | def call(params)
7 | self.format = :json
8 |
9 | @version = Octodmin::VERSION
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/posts/revert.rb:
--------------------------------------------------------------------------------
1 | require_relative "manage"
2 |
3 | module Octodmin::Controllers::Posts
4 | class Revert < Manage
5 | include Octodmin::Action
6 | expose :post
7 |
8 | def call(params)
9 | super
10 |
11 | @post.revert
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/sample/_layouts/page.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
9 |
10 |
11 | {{ content }}
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/controllers/posts/destroy.rb:
--------------------------------------------------------------------------------
1 | require_relative "manage"
2 |
3 | module Octodmin::Controllers::Posts
4 | class Destroy < Manage
5 | include Octodmin::Action
6 | expose :post
7 |
8 | def call(params)
9 | super
10 |
11 | @post.delete
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/posts/manage.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Posts
2 | class Manage
3 | def call(params)
4 | self.format = :json
5 | @post = Octodmin::Post.find(params[:id])
6 | halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/posts/restore.rb:
--------------------------------------------------------------------------------
1 | require_relative "manage"
2 |
3 | module Octodmin::Controllers::Posts
4 | class Restore < Manage
5 | include Octodmin::Action
6 | expose :post
7 |
8 | def call(params)
9 | super
10 |
11 | @post.restore
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/posts/index.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Posts
2 | class Index
3 | include Octodmin::Action
4 | expose :posts
5 |
6 | def call(params)
7 | self.format = :json
8 |
9 | site = Octodmin::Site.new
10 | @posts = site.posts
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | tmp
16 | .greenbar
17 | .powrc
18 | .ruby-gemset
19 | .ruby-version
20 | app/public
21 | bower_components
22 | _site
23 | .sass-cache
24 | .deploy
25 |
--------------------------------------------------------------------------------
/sample/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% include head.html %}
5 |
6 |
7 |
8 | {% include header.html %}
9 |
10 |
11 |
12 | {{ content }}
13 |
14 |
15 |
16 | {% include footer.html %}
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/spec/api/version_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "version" do
4 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
5 |
6 | describe "show" do
7 | before { get "/api/version" }
8 | subject { parse_json(last_response.body)["versions"] }
9 |
10 | it "returns version" do
11 | expect(subject).to eql(Octodmin::VERSION)
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/templates/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Loading...
5 |
6 |
7 |
8 |
9 |
10 | <%= yield %>
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/sample/_layouts/post.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
10 |
11 |
12 | {{ content }}
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/spec/frontend/home_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "home" do
4 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
5 |
6 | describe "home" do
7 | before { get "/" }
8 | subject { last_response.body }
9 |
10 | it "returns home" do
11 | expect(subject).to include("")
12 | expect(subject).to include("/assets/app.css")
13 | expect(subject).to include("/assets/app.js")
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/assets/javascripts/app.js.coffee:
--------------------------------------------------------------------------------
1 | #= require jquery
2 | #= require jquery-serialize-object
3 | #= require ajaxq
4 | #= require bootstrap
5 | #= require bootstrap-markdown
6 | #= require bootstrap.growl/bootstrap-growl
7 | #= require marked
8 | #= require spin.js/spin.js
9 | #= require react
10 | #= require react-loader
11 | #= require react-router
12 | #= require momentjs/moment
13 |
14 | #= require components/header
15 | #= require components/posts
16 | #= require components/router
17 |
--------------------------------------------------------------------------------
/sample/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: About
4 | permalink: /about/
5 | ---
6 |
7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](http://jekyllrb.com/)
8 |
9 | You can find the source code for the Jekyll new theme at: [github.com/jglovier/jekyll-new](https://github.com/jglovier/jekyll-new)
10 |
11 | You can find the source code for Jekyll at [github.com/jekyll/jekyll](https://github.com/jekyll/jekyll)
12 |
--------------------------------------------------------------------------------
/app/config/routes.rb:
--------------------------------------------------------------------------------
1 | namespace "api" do
2 | resource :version, only: [:show]
3 | resource :site, only: [:show]
4 | resources :posts do
5 | member do
6 | patch :restore
7 | patch :revert
8 | post :upload
9 | end
10 | end
11 | resources :syncs, only: [:create]
12 | resources :deploys, only: [:create]
13 | end
14 |
15 | if ENV["LOTUS_ENV"] != "production"
16 | mount Octodmin.sprockets, at: "/assets"
17 | end
18 |
19 | get "/posts*", to: "frontend#index"
20 | get "/", to: "frontend#index", as: :home
21 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "octodmin",
3 | "dependencies": {
4 | "jquery": "~2.1.3",
5 | "jquery-serialize-object": "~2.4.5",
6 | "ajaxq": "~0.0.2",
7 | "bootstrap-sass": "~3.3.2",
8 | "bootstrap-markdown": "~2.8.0",
9 | "bootstrap.growl": "~2.0.1",
10 | "marked": "~0.3.3",
11 | "spin.js": "~2.0.2",
12 | "react": "~0.12.0",
13 | "react-loader": "~1.1.0",
14 | "react-router": "~0.12.0",
15 | "fontawesome": "~4.3.0",
16 | "bootswatch": "~3.3.2",
17 | "momentjs": "~2.9.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/controllers/deploys/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Deploys
2 | class Create
3 | include Octodmin::Action
4 | expose :message
5 |
6 | def call(params)
7 | self.format = :json
8 |
9 | site = Octodmin::Site.new
10 | site.process
11 |
12 | options = site.config["octodmin"]["deploys"].first
13 | Octopress::Deploy.push(options)
14 |
15 | @message = "Deployed successfully"
16 | rescue SystemExit => e
17 | halt 400, JSON.dump(errors: [e.message])
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | gemspec
3 |
4 | gem "rake", "~> 10.0"
5 | gem "sprockets", "~> 3.0.0.beta"
6 | gem "bower", "~> 0.0.2"
7 | gem "sprockets-standalone", github: "krasnoukhov/sprockets-standalone"
8 | gem "coffee-react", "~> 2.4.0"
9 |
10 | group :test do
11 | gem "simplecov", "~> 0.9.0", require: false
12 | gem "coveralls", "~> 0.7.0", require: false
13 | gem "rspec", "~> 3.2.0"
14 | gem "json_spec", "~> 1.1.0"
15 | gem "rack-test", "~> 0.6.0"
16 | end
17 |
--------------------------------------------------------------------------------
/spec/api/site_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "site" do
4 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
5 |
6 | describe "show" do
7 | before { get "/api/site" }
8 | subject { parse_json(last_response.body)["sites"] }
9 |
10 | it "returns site" do
11 | expect(subject["title"]).to eql("Your awesome title")
12 | end
13 |
14 | it "returns octodmin config" do
15 | expect(subject["octodmin"]["front_matter"].keys).to eq(["layout", "title", "slug", "date", "custom"])
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/posts/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Posts
2 | class Create
3 | include Octodmin::Action
4 | expose :post
5 |
6 | params do
7 | param :title, presence: true
8 | end
9 |
10 | def call(params)
11 | self.format = :json
12 | halt 400, JSON.dump(errors: ["Required param `title` is not specified"]) unless params.valid?
13 |
14 | @post = Octodmin::Post.create(title: params[:title])
15 | halt 400, JSON.dump(errors: ["Post with specified `title` already exists"]) unless @post
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/posts/upload.rb:
--------------------------------------------------------------------------------
1 | require_relative "manage"
2 |
3 | module Octodmin::Controllers::Posts
4 | class Upload < Manage
5 | include Octodmin::Action
6 | expose :upload
7 |
8 | def call(params)
9 | super
10 |
11 | site = Octodmin::Site.new
12 | file = params[:file]
13 | dir = File.join(site.source, "octodmin", @post.identifier)
14 | path = File.join(dir, file["filename"])
15 |
16 | FileUtils.mkdir_p(dir)
17 | FileUtils.cp(file["tempfile"].path, path)
18 |
19 | @upload = path.sub(site.source, "")
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/frontend/assets_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "assets" do
4 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
5 |
6 | describe "css" do
7 | before { get "/assets/app.css" }
8 | subject { last_response.body }
9 |
10 | it "returns css" do
11 | expect(subject).to include("@charset")
12 | end
13 | end
14 |
15 | describe "js" do
16 | before { get "/assets/app.js" }
17 | subject { last_response.body }
18 |
19 | it "returns js" do
20 | expect(subject).to include("App = React.createClass")
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/sample/index.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
7 |
Posts
8 |
9 |
10 | {% for post in site.posts %}
11 | -
12 | {{ post.date | date: "%b %-d, %Y" }}
13 |
14 |
17 |
18 | {% endfor %}
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/_deploy.yml:
--------------------------------------------------------------------------------
1 | method: git # How do you want to deploy? git, rsync or s3.
2 | site_dir: _site # Location of your static site files.
3 |
4 | git_url: git@github.com:krasnoukhov/octodmin.git # remote repository url, e.g. git@github.com:username/repo_name
5 |
6 | # Note on git_branch:
7 | # If using GitHub project pages, set the branch to 'gh-pages'.
8 | # For GitHub user/organization pages or Heroku, set the branch to 'master'.
9 | #
10 | git_branch: gh-pages # Git branch where static site files are commited
11 |
12 |
13 | # remote_path: # Destination directory
14 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RACK_ENV"] = ENV["LOTUS_ENV"] = "test"
2 |
3 | require "rubygems"
4 | require "bundler/setup"
5 |
6 | Bundler.require(:test)
7 | require "rack/test"
8 |
9 | if ENV["COVERAGE"] == "true"
10 | require "simplecov"
11 | require "coveralls"
12 |
13 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
14 | SimpleCov::Formatter::HTMLFormatter,
15 | Coveralls::SimpleCov::Formatter
16 | ]
17 |
18 | SimpleCov.start do
19 | command_name "spec"
20 | add_filter "spec"
21 | end
22 | end
23 |
24 | $:.unshift "lib"
25 | require "octodmin"
26 | require "octodmin/app"
27 |
28 | RSpec.configure do |config|
29 | config.include Rack::Test::Methods
30 | config.include JsonSpec::Helpers
31 | end
32 |
--------------------------------------------------------------------------------
/app/controllers/posts/update.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Posts
2 | class Update
3 | include Octodmin::Action
4 | expose :post
5 |
6 | params do
7 | param :layout, presence: true
8 | param :title, presence: true
9 | param :slug, presence: true
10 | param :date, presence: true
11 | param :content, presence: true
12 | end
13 |
14 | def call(params)
15 | self.format = :json
16 |
17 | @post = Octodmin::Post.find(params.env["router.params"][:id])
18 | halt 400, JSON.dump(errors: ["Could not find post"]) unless @post
19 | halt 400, JSON.dump(errors: ["Required params are not specified"]) unless params.valid?
20 | @post.update(params.env["rack.request.form_hash"].dup)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | v0.3.5
2 | ------
3 |
4 | * Handle dates gracefully
5 |
6 | v0.3.4
7 | ------
8 |
9 | * Use `_raw` when rendering JSON, bump Lotus to 0.3.0
10 |
11 | v0.3.3
12 | ------
13 |
14 | * Make sync really delete files
15 |
16 | v0.3.2
17 | ------
18 |
19 | * Disable upload button when file is chosen
20 | * Fix preview layout
21 | * Optimize post initialization
22 |
23 | v0.3.1
24 | ------
25 |
26 | * Fix staging for those without octodmin dir
27 |
28 | v0.3.0
29 | ------
30 |
31 | * Improve preview
32 | * Implement file uploads
33 |
34 | v0.2.1
35 | ------
36 |
37 | * Bring back posts refreshing
38 |
39 | v0.2.0
40 | ------
41 |
42 | * Support Ruby 2.0
43 | * Disable posts refreshing because of race conditions
44 | * Various UI fixes
45 |
46 | v0.1.0
47 | ------
48 |
49 | * Initial version
50 |
--------------------------------------------------------------------------------
/sample/_includes/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | # Site settings
2 | source: sample
3 | title: Your awesome title
4 | email: your-email@domain.com
5 | description: > # this means to ignore newlines until "baseurl:"
6 | Write an awesome description for your new site here. You can edit this
7 | line in _config.yml. It will appear in your document head meta (for
8 | Google search results) and in your feed.xml site description.
9 | baseurl: "/octodmin" # the subpath of your site, e.g. /blog/
10 | url: "http://krasnoukhov.github.io" # the base hostname & protocol for your site
11 | twitter_username: jekyllrb
12 | github_username: jekyll
13 |
14 | # Build settings
15 | markdown: kramdown
16 |
17 | # Octopress settings
18 | post_ext: markdown
19 |
20 | # Octodmin settings
21 | octodmin:
22 | transliterate: ukrainian
23 | deploys:
24 | - config_file: _deploy.yml
25 | front_matter:
26 | custom:
27 | type: "text"
28 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "fileutils"
3 | require "rspec/core/rake_task"
4 | require "bundler/gem_tasks"
5 | require "sprockets/standalone"
6 |
7 | # Test tasks
8 | RSpec::Core::RakeTask.new(:spec)
9 |
10 | namespace :spec do
11 | task coverage: :"assets:remove" do
12 | ENV["COVERAGE"] = "true"
13 | Rake::Task["spec"].invoke
14 | end
15 | end
16 |
17 | task default: :spec
18 |
19 | # Release tasks
20 | Sprockets::Standalone::RakeTask.new(:assets) do |task, sprockets|
21 | require_relative "app/config/sprockets"
22 |
23 | task.assets = %w(app.js app.css *.eot *.svg *.ttf *.woff *woff2)
24 | task.sources = %w(app/assets)
25 | task.output = "app/public/assets"
26 | task.compress = true
27 | task.digest = false
28 | task.environment = Octodmin.sprockets
29 | end
30 |
31 | namespace :assets do
32 | task :remove do
33 | FileUtils.rm_r("./tmp/sprockets", force: true)
34 | FileUtils.rm_r("./app/public/assets", force: true)
35 | end
36 | end
37 |
38 | Rake::Task["build"].enhance([:"assets:remove", :"assets:compile"])
39 |
--------------------------------------------------------------------------------
/spec/api/deploys_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "deploys" do
4 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
5 |
6 | before do
7 | expect(Octopress::Deploy).to receive(:push).once
8 | end
9 |
10 | describe "create" do
11 | context "invalid" do
12 | before do
13 | allow(Octopress::Deploy).to receive(:push).and_raise(SystemExit, "Deploy error")
14 | post "/api/deploys"
15 | end
16 | subject { parse_json(last_response.body)["errors"] }
17 |
18 | it "returns errors" do
19 | expect(last_response).to_not be_ok
20 | expect(subject).to eql(["Deploy error"])
21 | end
22 | end
23 |
24 | context "valid" do
25 | before do
26 | allow(Octopress::Deploy).to receive(:push).and_return(nil)
27 | post "/api/deploys"
28 | end
29 | subject { parse_json(last_response.body)["deploys"] }
30 |
31 | it "returns errors" do
32 | expect(subject).to eql(["Deployed successfully"])
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/octodmin.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path("../lib", __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require "octodmin/version"
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "octodmin"
8 | spec.version = Octodmin::VERSION
9 | spec.authors = ["Dmitry Krasnoukhov"]
10 | spec.email = ["dmitry@krasnoukhov.com"]
11 | spec.summary = spec.description = %q{Content management for Jekyll blogs}
12 | spec.homepage = "https://github.com/krasnoukhov/octodmin"
13 | spec.license = "MIT"
14 |
15 | spec.files = (`git ls-files -z`.split("\x0") + `find app`.split("\n")).uniq
16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18 | spec.require_paths = ["lib"]
19 |
20 | spec.add_runtime_dependency "octopress", ">= 3.0.0.rc"
21 | spec.add_runtime_dependency "babosa", "~> 1.0"
22 | spec.add_runtime_dependency "lotusrb", ">= 0.3.0"
23 | spec.add_runtime_dependency "git", "~> 1.2.9"
24 | end
25 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Dmitry Krasnoukhov
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 |
--------------------------------------------------------------------------------
/app/config/sprockets.rb:
--------------------------------------------------------------------------------
1 | require "sprockets"
2 | require "coffee_script"
3 | require "coffee-react"
4 | require "bower"
5 |
6 | module Octodmin
7 | module CjsxProcessor
8 | VERSION = '1'
9 | SOURCE_VERSION = ::CoffeeScript::Source.version
10 |
11 | def self.cache_key
12 | @cache_key ||= [name, SOURCE_VERSION, VERSION].freeze
13 | end
14 |
15 | def self.call(input)
16 | data = input[:data]
17 | input[:cache].fetch(self.cache_key + [data]) do
18 | ::CoffeeScript.compile(::CoffeeReact.transform(data))
19 | end
20 | end
21 | end
22 |
23 | def self.sprockets
24 | assets_path = File.expand_path("../../assets", __FILE__)
25 | tmp_path = File.expand_path("../../../tmp", __FILE__)
26 |
27 | sprockets = ::Sprockets::Environment.new
28 | sprockets.append_path "#{assets_path}/stylesheets"
29 | sprockets.append_path "#{assets_path}/javascripts"
30 | sprockets.append_path "#{assets_path}/fonts"
31 | sprockets.append_path Bower.environment.directory
32 | sprockets.register_engine ".cjsx", CjsxProcessor
33 | sprockets.cache = Sprockets::Cache::FileStore.new(tmp_path)
34 | sprockets
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/sample/_includes/header.html:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/sample/css/main.scss:
--------------------------------------------------------------------------------
1 | ---
2 | # Only the main Sass file needs front matter (the dashes are enough)
3 | ---
4 | @charset "utf-8";
5 |
6 |
7 |
8 | // Our variables
9 | $base-font-family: Helvetica, Arial, sans-serif;
10 | $base-font-size: 16px;
11 | $small-font-size: $base-font-size * 0.875;
12 | $base-line-height: 1.5;
13 |
14 | $spacing-unit: 30px;
15 |
16 | $text-color: #111;
17 | $background-color: #fdfdfd;
18 | $brand-color: #2a7ae2;
19 |
20 | $grey-color: #828282;
21 | $grey-color-light: lighten($grey-color, 40%);
22 | $grey-color-dark: darken($grey-color, 25%);
23 |
24 | // Width of the content area
25 | $content-width: 800px;
26 |
27 | $on-palm: 600px;
28 | $on-laptop: 800px;
29 |
30 |
31 |
32 | // Using media queries with like this:
33 | // @include media-query($on-palm) {
34 | // .wrapper {
35 | // padding-right: $spacing-unit / 2;
36 | // padding-left: $spacing-unit / 2;
37 | // }
38 | // }
39 | @mixin media-query($device) {
40 | @media screen and (max-width: $device) {
41 | @content;
42 | }
43 | }
44 |
45 |
46 |
47 | // Import partials from `sass_dir` (defaults to `_sass`)
48 | @import
49 | "base",
50 | "layout",
51 | "syntax-highlighting"
52 | ;
53 |
--------------------------------------------------------------------------------
/app/octodmin.rb:
--------------------------------------------------------------------------------
1 | ENV["RACK_ENV"] = ENV["LOTUS_ENV"] ||= "production"
2 |
3 | require "octodmin"
4 | require "lotus"
5 | require "git"
6 | require "json"
7 | require "octopress-deploy"
8 |
9 | begin
10 | require_relative "./config/sprockets"
11 | rescue LoadError
12 | end
13 |
14 | module Octodmin
15 | class App < Lotus::Application
16 | class << self
17 | attr_accessor :dir
18 | end
19 |
20 | configure do
21 | root __dir__
22 |
23 | routes "config/routes"
24 | load_paths << [
25 | "controllers",
26 | "views",
27 | ]
28 |
29 | layout :application
30 | end
31 |
32 | # :nocov:
33 | configure :production do
34 | assets << [
35 | "public",
36 | Octodmin::Site.new.source
37 | ]
38 | serve_assets true
39 | handle_exceptions false
40 | end
41 |
42 | configure :development do
43 | handle_exceptions false
44 | end
45 | # :nocov:
46 |
47 | configure :test do
48 | handle_exceptions false
49 | end
50 |
51 | def initialize(dir = nil)
52 | raise "Please specify root dir" unless dir
53 | raise "Attempt to change root dir" if !self.class.dir.nil? && self.class.dir != dir
54 |
55 | self.class.dir = dir
56 | super()
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/sample/_posts/2015-01-29-welcome-to-jekyll.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Welcome to Jekyll!"
4 | date: 2015-01-29 18:00:17
5 | categories: jekyll update
6 | ---
7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.
8 |
9 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works.
10 |
11 | Jekyll also offers powerful support for code snippets:
12 |
13 | {% highlight ruby %}
14 | def print_hi(name)
15 | puts "Hi, #{name}"
16 | end
17 | print_hi('Tom')
18 | #=> prints 'Hi, Tom' to STDOUT.
19 | {% endhighlight %}
20 |
21 | Check out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help].
22 |
23 | [jekyll]: http://jekyllrb.com
24 | [jekyll-gh]: https://github.com/jekyll/jekyll
25 | [jekyll-help]: https://github.com/jekyll/jekyll-help
26 |
--------------------------------------------------------------------------------
/lib/octodmin/site.rb:
--------------------------------------------------------------------------------
1 | module Octodmin
2 | class Site
3 | attr_accessor :site
4 |
5 | DEFAULT_CONFIG = {
6 | "octodmin" => {
7 | "transliterate" => "latin",
8 | "front_matter" => {
9 | "layout" => {
10 | "type" => "text",
11 | },
12 | "title" => {
13 | "type" => "text",
14 | },
15 | "slug" => {
16 | "type" => "text",
17 | },
18 | "date" => {
19 | "type" => "text",
20 | },
21 | },
22 | },
23 | }
24 |
25 | def initialize
26 | @site = Jekyll::Site.new(Jekyll.configuration)
27 | end
28 |
29 | def source
30 | @site.source
31 | end
32 |
33 | def config
34 | @config ||= Jekyll::Utils.deep_merge_hashes(DEFAULT_CONFIG, @site.config)
35 | end
36 |
37 | def serializable_hash
38 | config
39 | end
40 |
41 | def posts
42 | reset
43 | @site.read
44 | @site.posts.sort_by(&:date).last(20).reverse.map { |post| Post.new(self, post) }
45 | end
46 |
47 | def reset
48 | @status = nil
49 | @site.reset
50 | self
51 | end
52 |
53 | def status
54 | @status ||= Git.open(Octodmin::App.dir).status
55 | end
56 |
57 | def process
58 | @site.process
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/sample/feed.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: null
3 | ---
4 |
5 |
6 |
7 | {{ site.title | xml_escape }}
8 | {{ site.description | xml_escape }}
9 | {{ site.url }}{{ site.baseurl }}/
10 |
11 | {{ site.time | date_to_rfc822 }}
12 | {{ site.time | date_to_rfc822 }}
13 | Jekyll v{{ jekyll.version }}
14 | {% for post in site.posts limit:10 %}
15 | -
16 | {{ post.title | xml_escape }}
17 | {{ post.content | xml_escape }}
18 | {{ post.date | date_to_rfc822 }}
19 | {{ post.url | prepend: site.baseurl | prepend: site.url }}
20 | {{ post.url | prepend: site.baseurl | prepend: site.url }}
21 | {% for tag in post.tags %}
22 | {{ tag | xml_escape }}
23 | {% endfor %}
24 | {% for cat in post.categories %}
25 | {{ cat | xml_escape }}
26 | {% endfor %}
27 |
28 | {% endfor %}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/app.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 | $fa-font-path: "/assets/fontawesome/fonts";
3 | @import "bootswatch/paper/variables";
4 | $icon-font-path: "/assets/bootstrap/fonts/";
5 | //= require bootstrap-markdown
6 | @import
7 | "bootstrap-sass/assets/stylesheets/bootstrap",
8 | "bootswatch/paper/bootswatch",
9 | "fontawesome/scss/font-awesome"
10 | ;
11 |
12 | body {
13 | padding-bottom: 20px;
14 | }
15 |
16 | .growl-animated {
17 | padding: 10px 35px 10px 15px;
18 | }
19 |
20 | a, button, input, textarea, *:focus {
21 | outline: 0 !important;
22 | outline-style: none !important;
23 | }
24 |
25 | .navbar {
26 | margin-bottom: 20px;
27 |
28 | .navbar-right {
29 | margin-right: 0;
30 |
31 | .btn {
32 | margin-left: 10px;
33 | }
34 | }
35 | }
36 |
37 | .panel {
38 | &:not(.panel-default) {
39 | .panel-heading {
40 | color: #fff;
41 | }
42 | }
43 |
44 | .panel-heading {
45 | line-height: 26px;
46 |
47 | h3 {
48 | font-size: 26px;
49 | }
50 | }
51 |
52 | .panel-body {
53 | .excerpt {
54 | :first-child {
55 | margin-top: 0;
56 | }
57 |
58 | :last-child {
59 | margin-bottom: 0;
60 | }
61 | }
62 |
63 | .buttons {
64 | text-align: center;
65 | }
66 | }
67 | }
68 |
69 | .post-edit {
70 | .md-editor {
71 | .md-header {
72 | margin-bottom: 15px;
73 | }
74 | }
75 |
76 | .md-content {
77 | width: 100%;
78 | }
79 |
80 | .md-preview {
81 | overflow: auto;
82 | }
83 |
84 | .buttons {
85 | .btn {
86 | margin-left: 10px;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/router.js.cjsx:
--------------------------------------------------------------------------------
1 | # @cjsx React.DOM
2 |
3 | @Router = ReactRouter
4 | @Link = Router.Link
5 | @Routes = Router.Routes
6 | @Route = Router.Route
7 | @DefaultRoute = Router.DefaultRoute
8 | @RouteHandler = Router.RouteHandler
9 |
10 | @growlSuccess =
11 | type: "success"
12 | delay: 10000
13 |
14 | @growlError =
15 | type: "danger"
16 | delay: 60000
17 |
18 | @Container = React.createClass(
19 | render: ->
20 |
21 | {@props.children}
22 |
23 | )
24 |
25 | @App = React.createClass(
26 | getInitialState: ->
27 | { site: null }
28 |
29 | fetchSite: ->
30 | $.getq("default", "/api/site").done(@handleSuccess).fail(@handleError)
31 |
32 | handleSuccess: (response) ->
33 | @setState(site: response.sites)
34 | $("title").text("Octodmin – #{response.sites.title}")
35 |
36 | handleError: (error) ->
37 | $.growl("Could not load site: #{error.statusText} (#{error.status})", growlError)
38 |
39 | componentWillMount: ->
40 | @fetchSite()
41 | $(document).on("fetchSite", @fetchSite)
42 |
43 | componentWillUnmount: ->
44 | $(document).off("fetchSite", @fetchSite)
45 |
46 | render: ->
47 |
48 |
49 |
50 |
51 |
52 |
53 | )
54 |
55 | routes =
56 |
57 |
58 |
59 |
60 |
61 | Router.run(routes, Router.HistoryLocation, (Handler) ->
62 | React.render(, document.getElementById("app"))
63 | )
64 |
--------------------------------------------------------------------------------
/app/controllers/syncs/create.rb:
--------------------------------------------------------------------------------
1 | module Octodmin::Controllers::Syncs
2 | class Create
3 | include Octodmin::Action
4 | expose :message
5 |
6 | def call(params)
7 | self.format = :json
8 |
9 | site = Octodmin::Site.new
10 | git = Git.open(Octodmin::App.dir)
11 |
12 | # Add files to commit stage
13 | stage(site, git)
14 |
15 | # Pull changes
16 | git.pull
17 |
18 | # Commit and push changes if any
19 | sync(site, git)
20 | rescue Git::GitExecuteError => e
21 | halt 400, JSON.dump(errors: [e.message])
22 | ensure
23 | git.reset(".")
24 | end
25 |
26 | private
27 |
28 | def deleted(site)
29 | site.status.deleted.keys.map { |path| File.join(Octodmin::App.dir, path) }
30 | end
31 |
32 | def stage(site, git)
33 | # Posts
34 | site.posts.each do |post|
35 | path = File.join(site.source, post.path)
36 | git.add(path) unless deleted(site).include?(path)
37 | end
38 |
39 | # Uploads
40 | dir = File.join(site.source, "octodmin")
41 | git.add(dir) if Dir.exists?(dir)
42 | end
43 |
44 | def paths(site, git)
45 | status = site.reset.status
46 | keys = status.changed.keys + status.added.keys + status.deleted.keys
47 |
48 | keys.sort.reverse.map do |path|
49 | File.join(Octodmin::App.dir, path).sub(File.join(site.source, ""), "")
50 | end
51 | end
52 |
53 | def sync(site, git)
54 | staged = paths(site, git)
55 |
56 | if staged.any?
57 | @message = "Octodmin sync for #{staged.count} file#{"s" if staged.count > 1}"
58 | @message += "\n\n#{staged.join("\n")}"
59 |
60 | deleted(site).each { |path| File.delete(path) }
61 | git.commit(@message)
62 | git.push
63 | else
64 | @message = "Everything is up-to-date"
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/header.js.cjsx:
--------------------------------------------------------------------------------
1 | # @cjsx React.DOM
2 |
3 | @Header = React.createClass(
4 | propTypes:
5 | site: React.PropTypes.object.isRequired
6 |
7 | getInitialState: ->
8 | { loading: false }
9 |
10 | handleSync: ->
11 | return if @state.loading
12 | @setState(loading: true)
13 | $.postq("default", "/api/syncs").always(@handleResponse).done(@handleSyncSuccess).fail(@handleError)
14 |
15 | handleDeploy: ->
16 | return if @state.loading
17 | @setState(loading: true)
18 | $.postq("default", "/api/deploys").always(@handleResponse).done(@handleDeploySuccess).fail(@handleError)
19 |
20 | handleResponse: ->
21 | @setState(loading: false)
22 |
23 | handleSyncSuccess: (response) ->
24 | $.growl(response.syncs.join("\n").replace(/\n/g, "
"), growlSuccess)
25 | $(document).trigger("fetchPosts")
26 |
27 | handleDeploySuccess: (response) ->
28 | $.growl(response.deploys.join("\n"), growlSuccess)
29 | $(document).trigger("fetchPosts")
30 |
31 | handleError: (error) ->
32 | $.growl(error.responseJSON?.errors.join("\n").replace(/\n/g, "
") || error.statusText, growlError)
33 |
34 | render: ->
35 |
36 |
58 |
59 | )
60 |
--------------------------------------------------------------------------------
/sample/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
56 |
--------------------------------------------------------------------------------
/spec/api/syncs_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "syncs" do
4 | let(:date) { Date.today.strftime("%Y-%m-%d") }
5 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
6 |
7 | before do
8 | allow_any_instance_of(Git::Base).to receive(:pull).and_return(nil)
9 | allow_any_instance_of(Git::Base).to receive(:push).and_return(nil)
10 | end
11 |
12 | describe "create" do
13 | context "invalid" do
14 | before do
15 | allow_any_instance_of(Git::Base).to receive(:pull).and_raise(Git::GitExecuteError, "Git error")
16 | expect_any_instance_of(Git::Base).to receive(:pull).once
17 |
18 | post "/api/posts", title: "Blah"
19 | post "/api/syncs"
20 | end
21 | after { File.delete("sample/_posts/#{date}-blah.markdown") }
22 | subject { parse_json(last_response.body)["errors"] }
23 |
24 | it "returns errors" do
25 | expect(last_response).to_not be_ok
26 | expect(subject).to eql(["Git error"])
27 | end
28 | end
29 |
30 | context "valid" do
31 | context "no changes" do
32 | before do
33 | expect_any_instance_of(Git::Base).to receive(:pull).once
34 | post "/api/syncs"
35 | end
36 | subject { parse_json(last_response.body)["syncs"] }
37 |
38 | it "returns errors" do
39 | expect(subject).to eql(["Everything is up-to-date"])
40 | end
41 | end
42 |
43 | context "with changes" do
44 | before do
45 | allow_any_instance_of(Git::Base).to receive(:commit).and_return(nil)
46 | expect_any_instance_of(Git::Base).to receive(:pull).once
47 | expect_any_instance_of(Git::Base).to receive(:commit).once
48 | expect_any_instance_of(Git::Base).to receive(:push).once
49 |
50 | # Create post
51 | post "/api/posts", title: "Yo"
52 |
53 | # Update post
54 | patch "/api/posts/2015-01-30-test", {
55 | layout: "other",
56 | title: "Test",
57 | slug: "test",
58 | date: "2015-01-30 18:10:00",
59 | content: "### WOW",
60 | }
61 |
62 | # Delete post
63 | delete "/api/posts/2015-01-29-welcome-to-jekyll"
64 |
65 | # Upload file
66 | post "/api/posts/2015-01-30-test/upload", {
67 | file: Rack::Test::UploadedFile.new("spec/fixtures/ear.png")
68 | }
69 |
70 | post "/api/syncs"
71 | end
72 | after do
73 | File.delete("sample/_posts/#{date}-yo.markdown")
74 | git = Git.open(Octodmin::App.dir)
75 | git.checkout("sample/_posts/2015-01-30-test.markdown")
76 | git.checkout("sample/_posts/2015-01-29-welcome-to-jekyll.markdown")
77 | File.delete("sample/octodmin/2015-01-30-test/ear.png")
78 | end
79 | subject { parse_json(last_response.body)["syncs"] }
80 |
81 | it "returns syncs" do
82 | expect(File.exists?("sample/_posts/2015-01-29-welcome-to-jekyll.markdown")).to be_falsey
83 |
84 | expect(subject).to eql([[
85 | "Octodmin sync for 4 files\n",
86 | "octodmin/2015-01-30-test/ear.png",
87 | "_posts/#{date}-yo.markdown",
88 | "_posts/2015-01-30-test.markdown",
89 | "_posts/2015-01-29-welcome-to-jekyll.markdown",
90 | ].join("\n")])
91 | end
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/sample/_sass/_syntax-highlighting.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Syntax highlighting styles
3 | */
4 | .highlight {
5 | background: #fff;
6 | @extend %vertical-rhythm;
7 |
8 | .c { color: #998; font-style: italic } // Comment
9 | .err { color: #a61717; background-color: #e3d2d2 } // Error
10 | .k { font-weight: bold } // Keyword
11 | .o { font-weight: bold } // Operator
12 | .cm { color: #998; font-style: italic } // Comment.Multiline
13 | .cp { color: #999; font-weight: bold } // Comment.Preproc
14 | .c1 { color: #998; font-style: italic } // Comment.Single
15 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
16 | .gd { color: #000; background-color: #fdd } // Generic.Deleted
17 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
18 | .ge { font-style: italic } // Generic.Emph
19 | .gr { color: #a00 } // Generic.Error
20 | .gh { color: #999 } // Generic.Heading
21 | .gi { color: #000; background-color: #dfd } // Generic.Inserted
22 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
23 | .go { color: #888 } // Generic.Output
24 | .gp { color: #555 } // Generic.Prompt
25 | .gs { font-weight: bold } // Generic.Strong
26 | .gu { color: #aaa } // Generic.Subheading
27 | .gt { color: #a00 } // Generic.Traceback
28 | .kc { font-weight: bold } // Keyword.Constant
29 | .kd { font-weight: bold } // Keyword.Declaration
30 | .kp { font-weight: bold } // Keyword.Pseudo
31 | .kr { font-weight: bold } // Keyword.Reserved
32 | .kt { color: #458; font-weight: bold } // Keyword.Type
33 | .m { color: #099 } // Literal.Number
34 | .s { color: #d14 } // Literal.String
35 | .na { color: #008080 } // Name.Attribute
36 | .nb { color: #0086B3 } // Name.Builtin
37 | .nc { color: #458; font-weight: bold } // Name.Class
38 | .no { color: #008080 } // Name.Constant
39 | .ni { color: #800080 } // Name.Entity
40 | .ne { color: #900; font-weight: bold } // Name.Exception
41 | .nf { color: #900; font-weight: bold } // Name.Function
42 | .nn { color: #555 } // Name.Namespace
43 | .nt { color: #000080 } // Name.Tag
44 | .nv { color: #008080 } // Name.Variable
45 | .ow { font-weight: bold } // Operator.Word
46 | .w { color: #bbb } // Text.Whitespace
47 | .mf { color: #099 } // Literal.Number.Float
48 | .mh { color: #099 } // Literal.Number.Hex
49 | .mi { color: #099 } // Literal.Number.Integer
50 | .mo { color: #099 } // Literal.Number.Oct
51 | .sb { color: #d14 } // Literal.String.Backtick
52 | .sc { color: #d14 } // Literal.String.Char
53 | .sd { color: #d14 } // Literal.String.Doc
54 | .s2 { color: #d14 } // Literal.String.Double
55 | .se { color: #d14 } // Literal.String.Escape
56 | .sh { color: #d14 } // Literal.String.Heredoc
57 | .si { color: #d14 } // Literal.String.Interpol
58 | .sx { color: #d14 } // Literal.String.Other
59 | .sr { color: #009926 } // Literal.String.Regex
60 | .s1 { color: #d14 } // Literal.String.Single
61 | .ss { color: #990073 } // Literal.String.Symbol
62 | .bp { color: #999 } // Name.Builtin.Pseudo
63 | .vc { color: #008080 } // Name.Variable.Class
64 | .vg { color: #008080 } // Name.Variable.Global
65 | .vi { color: #008080 } // Name.Variable.Instance
66 | .il { color: #099 } // Literal.Number.Integer.Long
67 | }
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Octodmin
2 |
3 | Content management for Jekyll blogs
4 |
5 | [](http://badge.fury.io/rb/octodmin)
6 | [](http://travis-ci.org/krasnoukhov/octodmin?branch=master)
7 | [](https://coveralls.io/r/krasnoukhov/octodmin?branch=master)
8 | [](https://codeclimate.com/github/krasnoukhov/octodmin)
9 | [](https://gemnasium.com/krasnoukhov/octodmin)
10 |
11 | ## Installation
12 |
13 | Add this line to your Jekyll project's Gemfile:
14 |
15 | ```ruby
16 | gem 'octodmin'
17 | ```
18 |
19 | And then execute:
20 |
21 | ```bash
22 | $ bundle
23 | ```
24 |
25 | ## Usage
26 |
27 | Octodmin assumes that there is a `_config.yml` with Jekyll
28 | configuration.
29 |
30 | Add `config.ru` to your project:
31 |
32 | ```ruby
33 | require "octodmin/app"
34 | run Octodmin::App.new(__dir__)
35 | ```
36 |
37 | Run it as a Rack application:
38 |
39 | ```bash
40 | $ rackup
41 | ```
42 |
43 | ## Configuration
44 |
45 | Octodmin can be configured using `_config.yml`. For example:
46 |
47 | ```yaml
48 | # Octodmin settings
49 | octodmin:
50 | transliterate: ukrainian
51 | deploys:
52 | - config_file: _deploy.yml
53 | front_matter:
54 | custom:
55 | type: "text"
56 | ```
57 |
58 | Valid options:
59 |
60 | `transliterate`: use any of [babosa](https://github.com/norman/babosa#locale-sensitive-transliteration-with-support-for-many-languages)'s
61 | languages for slug transliteration. Default is `latin`
62 |
63 | `deploys`: if you use `octopress-deploy`, specify your deploy configuration
64 | file to get a "Deploy" button in Octodmin.
65 |
66 | `front_matter`: if you use custom front matter attributes, specify all
67 | of them to extend the edit form with corresponding inputs.
68 |
69 | Please note that Octodmin uses Octopress internally, so make sure you configure it
70 | accordingly. For example:
71 |
72 | ```yaml
73 | # Octopress
74 | post_ext: markdown
75 | post_layout: post
76 | ```
77 |
78 | ## Deployment
79 |
80 | Since Octodmin is a simple Rack app, use your favorite Ruby application server.
81 | For example, add `puma` to Gemfile, run `bundle`, and then `rackup`.
82 | That's it.
83 |
84 | When deploying Octodmin to a remote server, make sure you're able to run
85 | `git pull`, `git push` and `octopress deploy` (if needed) successfully
86 | in the shell of your remote user.
87 |
88 | For basic HTTP authentication, use `Rack::Auth::Basic`.
89 | Example for your `config.ru`:
90 |
91 | ```ruby
92 | use Rack::Auth::Basic, "Octodmin" do |username, password|
93 | [username, password] == [ENV["USERNAME"], ENV["PASSWORD"]]
94 | end
95 | ```
96 |
97 | Just set ENV variables and you're good to go.
98 |
99 | ## Development and testing
100 |
101 | You would need `npm` and `bower`. Run `bower install` to install asset
102 | dependencies.
103 |
104 | Run `rackup` to start the development server.
105 |
106 | Run `rspec` to run tests.
107 |
108 | ## Contributing
109 |
110 | 1. Fork it
111 | 2. Create your feature branch (`git checkout -b my-new-feature`)
112 | 3. Commit your changes (`git commit -am 'Add some feature'`)
113 | 4. Push to the branch (`git push origin my-new-feature`)
114 | 5. Create new Pull Request
115 |
116 | ## LICENSE
117 |
118 | The MIT License
119 |
--------------------------------------------------------------------------------
/sample/_sass/_base.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Reset some basic elements
3 | */
4 | body, h1, h2, h3, h4, h5, h6,
5 | p, blockquote, pre, hr,
6 | dl, dd, ol, ul, figure {
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 |
12 |
13 | /**
14 | * Basic styling
15 | */
16 | body {
17 | font-family: $base-font-family;
18 | font-size: $base-font-size;
19 | line-height: $base-line-height;
20 | font-weight: 300;
21 | color: $text-color;
22 | background-color: $background-color;
23 | -webkit-text-size-adjust: 100%;
24 | }
25 |
26 |
27 |
28 | /**
29 | * Set `margin-bottom` to maintain vertical rhythm
30 | */
31 | h1, h2, h3, h4, h5, h6,
32 | p, blockquote, pre,
33 | ul, ol, dl, figure,
34 | %vertical-rhythm {
35 | margin-bottom: $spacing-unit / 2;
36 | }
37 |
38 |
39 |
40 | /**
41 | * Images
42 | */
43 | img {
44 | max-width: 100%;
45 | vertical-align: middle;
46 | }
47 |
48 |
49 |
50 | /**
51 | * Figures
52 | */
53 | figure > img {
54 | display: block;
55 | }
56 |
57 | figcaption {
58 | font-size: $small-font-size;
59 | }
60 |
61 |
62 |
63 | /**
64 | * Lists
65 | */
66 | ul, ol {
67 | margin-left: $spacing-unit;
68 | }
69 |
70 | li {
71 | > ul,
72 | > ol {
73 | margin-bottom: 0;
74 | }
75 | }
76 |
77 |
78 |
79 | /**
80 | * Headings
81 | */
82 | h1, h2, h3, h4, h5, h6 {
83 | font-weight: 300;
84 | }
85 |
86 |
87 |
88 | /**
89 | * Links
90 | */
91 | a {
92 | color: $brand-color;
93 | text-decoration: none;
94 |
95 | &:visited {
96 | color: darken($brand-color, 15%);
97 | }
98 |
99 | &:hover {
100 | color: $text-color;
101 | text-decoration: underline;
102 | }
103 | }
104 |
105 |
106 |
107 | /**
108 | * Blockquotes
109 | */
110 | blockquote {
111 | color: $grey-color;
112 | border-left: 4px solid $grey-color-light;
113 | padding-left: $spacing-unit / 2;
114 | font-size: 18px;
115 | letter-spacing: -1px;
116 | font-style: italic;
117 |
118 | > :last-child {
119 | margin-bottom: 0;
120 | }
121 | }
122 |
123 |
124 |
125 | /**
126 | * Code formatting
127 | */
128 | pre,
129 | code {
130 | font-size: 15px;
131 | border: 1px solid $grey-color-light;
132 | border-radius: 3px;
133 | background-color: #eef;
134 | }
135 |
136 | code {
137 | padding: 1px 5px;
138 | }
139 |
140 | pre {
141 | padding: 8px 12px;
142 | overflow-x: scroll;
143 |
144 | > code {
145 | border: 0;
146 | padding-right: 0;
147 | padding-left: 0;
148 | }
149 | }
150 |
151 |
152 |
153 | /**
154 | * Wrapper
155 | */
156 | .wrapper {
157 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
158 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
159 | margin-right: auto;
160 | margin-left: auto;
161 | padding-right: $spacing-unit;
162 | padding-left: $spacing-unit;
163 | @extend %clearfix;
164 |
165 | @include media-query($on-laptop) {
166 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
167 | max-width: calc(#{$content-width} - (#{$spacing-unit}));
168 | padding-right: $spacing-unit / 2;
169 | padding-left: $spacing-unit / 2;
170 | }
171 | }
172 |
173 |
174 |
175 | /**
176 | * Clearfix
177 | */
178 | %clearfix {
179 |
180 | &:after {
181 | content: "";
182 | display: table;
183 | clear: both;
184 | }
185 | }
186 |
187 |
188 |
189 | /**
190 | * Icons
191 | */
192 | .icon {
193 |
194 | > svg {
195 | display: inline-block;
196 | width: 16px;
197 | height: 16px;
198 | vertical-align: middle;
199 |
200 | path {
201 | fill: $grey-color;
202 | }
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/lib/octodmin/post.rb:
--------------------------------------------------------------------------------
1 | module Octodmin
2 | class Post
3 | attr_accessor :post
4 |
5 | ATTRIBUTES_FOR_SERIALIZAION = Jekyll::Post::ATTRIBUTES_FOR_LIQUID - %w[
6 | previous
7 | next
8 | ]
9 |
10 | def self.find(id)
11 | site = Octodmin::Site.new
12 | site.posts.find { |post| post.identifier == id }
13 | end
14 |
15 | def self.create(options = {})
16 | site = Octodmin::Site.new
17 |
18 | # Prepare slug
19 | options["slug"] = slug_for(site, options[:title])
20 |
21 | # Create post
22 | post = Octopress::Post.new(Octopress.site, Jekyll::Utils.stringify_hash_keys(options))
23 | post.write
24 |
25 | site.posts.first
26 | rescue RuntimeError
27 | end
28 |
29 | def initialize(site, post)
30 | @site = site
31 | @post = post
32 | end
33 |
34 | def path
35 | @post.path
36 | end
37 |
38 | def identifier
39 | path.split("/").last.split(".").first
40 | end
41 |
42 | def serializable_hash
43 | hash = @post.to_liquid(ATTRIBUTES_FOR_SERIALIZAION).dup
44 |
45 | excerpt = Jekyll::Excerpt.new(@post)
46 | excerpt.do_layout({ "site" => @site.config }, {})
47 |
48 | hash.merge(
49 | excerpt: excerpt.content,
50 | identifier: identifier,
51 | slug: @post.slug,
52 | added: added?,
53 | changed: changed?,
54 | deleted: deleted?,
55 | )
56 | end
57 |
58 | def update(params)
59 | # Remove old post
60 | delete
61 |
62 | # Init the new one
63 | octopost = Octopress::Post.new(Octopress.site, {
64 | "path" => @post.path,
65 | "date" => params["date"],
66 | "slug" => params["slug"],
67 | "title" => params["title"],
68 | "force" => true,
69 | })
70 |
71 | options = Hash[@site.config["octodmin"]["front_matter"].keys.map do |key|
72 | [key, params[key]]
73 | end]
74 |
75 | content = params["content"].gsub("\r\n", "\n").strip
76 | result = "---\n#{options.map { |k, v| "#{k}: \"#{v}\"" }.join("\n")}\n---\n\n#{content}\n"
77 | octopost.instance_variable_set(:@content, result)
78 | octopost.write
79 |
80 | @post = post_for(octopost)
81 |
82 | # Add/reset post just in case
83 | git = Git.open(Octodmin::App.dir)
84 | git.add(octopost.path)
85 | git.reset(octopost.path)
86 | end
87 |
88 | def delete
89 | octopost = octopost_for(@post)
90 | git = Git.open(Octodmin::App.dir)
91 | git.lib.send(:command, "rm", ["-f", "--cached", octopost.path])
92 | rescue Git::GitExecuteError
93 | File.delete(octopost.path)
94 | end
95 |
96 | def restore
97 | return unless deleted?
98 |
99 | octopost = octopost_for(@post)
100 | git = Git.open(Octodmin::App.dir)
101 | git.add(octopost.path)
102 |
103 | @site.reset
104 | @post = post_for(octopost)
105 | end
106 |
107 | def revert
108 | return unless changed?
109 |
110 | octopost = octopost_for(@post)
111 | git = Git.open(Octodmin::App.dir)
112 | git.reset(octopost.path)
113 | git.checkout(octopost.path)
114 |
115 | @site.reset
116 | @post = post_for(octopost)
117 | end
118 |
119 | private
120 |
121 | def self.slug_for(site, title)
122 | title.to_slug.transliterate(
123 | site.config["octodmin"]["transliterate"]
124 | ).normalize.to_s
125 | end
126 |
127 | def octopost_for(post)
128 | Octopress::Post.new(Octopress.site, {
129 | "path" => post.path,
130 | "date" => post.date,
131 | "slug" => post.slug,
132 | "title" => post.title,
133 | })
134 | end
135 |
136 | def post_for(octopost)
137 | @site.posts.find do |post|
138 | File.join(@site.site.source, post.post.path) == octopost.path
139 | end.post
140 | end
141 |
142 | def has_status?(post, status)
143 | path = File.join(@site.source, post.path)
144 | changed = @site.status.public_send(status).keys
145 | paths = changed.map { |path| File.join(Octodmin::App.dir, path) }
146 | paths.include?(path)
147 | end
148 |
149 | def added?
150 | has_status?(@post, :untracked) && !changed? && !deleted?
151 | end
152 |
153 | def changed?
154 | has_status?(@post, :changed)
155 | end
156 |
157 | def deleted?
158 | has_status?(@post, :deleted)
159 | end
160 | end
161 | end
162 |
--------------------------------------------------------------------------------
/sample/_sass/_layout.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Site header
3 | */
4 | .site-header {
5 | border-top: 5px solid $grey-color-dark;
6 | border-bottom: 1px solid $grey-color-light;
7 | min-height: 56px;
8 |
9 | // Positioning context for the mobile navigation icon
10 | position: relative;
11 | }
12 |
13 | .site-title {
14 | font-size: 26px;
15 | line-height: 56px;
16 | letter-spacing: -1px;
17 | margin-bottom: 0;
18 | float: left;
19 |
20 | &,
21 | &:visited {
22 | color: $grey-color-dark;
23 | }
24 | }
25 |
26 | .site-nav {
27 | float: right;
28 | line-height: 56px;
29 |
30 | .menu-icon {
31 | display: none;
32 | }
33 |
34 | .page-link {
35 | color: $text-color;
36 | line-height: $base-line-height;
37 |
38 | // Gaps between nav items, but not on the first one
39 | &:not(:first-child) {
40 | margin-left: 20px;
41 | }
42 | }
43 |
44 | @include media-query($on-palm) {
45 | position: absolute;
46 | top: 9px;
47 | right: 30px;
48 | background-color: $background-color;
49 | border: 1px solid $grey-color-light;
50 | border-radius: 5px;
51 | text-align: right;
52 |
53 | .menu-icon {
54 | display: block;
55 | float: right;
56 | width: 36px;
57 | height: 26px;
58 | line-height: 0;
59 | padding-top: 10px;
60 | text-align: center;
61 |
62 | > svg {
63 | width: 18px;
64 | height: 15px;
65 |
66 | path {
67 | fill: $grey-color-dark;
68 | }
69 | }
70 | }
71 |
72 | .trigger {
73 | clear: both;
74 | display: none;
75 | }
76 |
77 | &:hover .trigger {
78 | display: block;
79 | padding-bottom: 5px;
80 | }
81 |
82 | .page-link {
83 | display: block;
84 | padding: 5px 10px;
85 | }
86 | }
87 | }
88 |
89 |
90 |
91 | /**
92 | * Site footer
93 | */
94 | .site-footer {
95 | border-top: 1px solid $grey-color-light;
96 | padding: $spacing-unit 0;
97 | }
98 |
99 | .footer-heading {
100 | font-size: 18px;
101 | margin-bottom: $spacing-unit / 2;
102 | }
103 |
104 | .contact-list,
105 | .social-media-list {
106 | list-style: none;
107 | margin-left: 0;
108 | }
109 |
110 | .footer-col-wrapper {
111 | font-size: 15px;
112 | color: $grey-color;
113 | margin-left: -$spacing-unit / 2;
114 | @extend %clearfix;
115 | }
116 |
117 | .footer-col {
118 | float: left;
119 | margin-bottom: $spacing-unit / 2;
120 | padding-left: $spacing-unit / 2;
121 | }
122 |
123 | .footer-col-1 {
124 | width: -webkit-calc(35% - (#{$spacing-unit} / 2));
125 | width: calc(35% - (#{$spacing-unit} / 2));
126 | }
127 |
128 | .footer-col-2 {
129 | width: -webkit-calc(20% - (#{$spacing-unit} / 2));
130 | width: calc(20% - (#{$spacing-unit} / 2));
131 | }
132 |
133 | .footer-col-3 {
134 | width: -webkit-calc(45% - (#{$spacing-unit} / 2));
135 | width: calc(45% - (#{$spacing-unit} / 2));
136 | }
137 |
138 | @include media-query($on-laptop) {
139 | .footer-col-1,
140 | .footer-col-2 {
141 | width: -webkit-calc(50% - (#{$spacing-unit} / 2));
142 | width: calc(50% - (#{$spacing-unit} / 2));
143 | }
144 |
145 | .footer-col-3 {
146 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
147 | width: calc(100% - (#{$spacing-unit} / 2));
148 | }
149 | }
150 |
151 | @include media-query($on-palm) {
152 | .footer-col {
153 | float: none;
154 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
155 | width: calc(100% - (#{$spacing-unit} / 2));
156 | }
157 | }
158 |
159 |
160 |
161 | /**
162 | * Page content
163 | */
164 | .page-content {
165 | padding: $spacing-unit 0;
166 | }
167 |
168 | .page-heading {
169 | font-size: 20px;
170 | }
171 |
172 | .post-list {
173 | margin-left: 0;
174 | list-style: none;
175 |
176 | > li {
177 | margin-bottom: $spacing-unit;
178 | }
179 | }
180 |
181 | .post-meta {
182 | font-size: $small-font-size;
183 | color: $grey-color;
184 | }
185 |
186 | .post-link {
187 | display: block;
188 | font-size: 24px;
189 | }
190 |
191 |
192 |
193 | /**
194 | * Posts
195 | */
196 | .post-header {
197 | margin-bottom: $spacing-unit;
198 | }
199 |
200 | .post-title {
201 | font-size: 42px;
202 | letter-spacing: -1px;
203 | line-height: 1;
204 |
205 | @include media-query($on-laptop) {
206 | font-size: 36px;
207 | }
208 | }
209 |
210 | .post-content {
211 | margin-bottom: $spacing-unit;
212 |
213 | h2 {
214 | font-size: 32px;
215 |
216 | @include media-query($on-laptop) {
217 | font-size: 28px;
218 | }
219 | }
220 |
221 | h3 {
222 | font-size: 26px;
223 |
224 | @include media-query($on-laptop) {
225 | font-size: 22px;
226 | }
227 | }
228 |
229 | h4 {
230 | font-size: 20px;
231 |
232 | @include media-query($on-laptop) {
233 | font-size: 18px;
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/posts.js.cjsx:
--------------------------------------------------------------------------------
1 | # @cjsx React.DOM
2 |
3 | @Posts = React.createClass(
4 | propTypes:
5 | site: React.PropTypes.object.isRequired
6 |
7 | getInitialState: ->
8 | { loading: false, posts: null }
9 |
10 | fetchPosts: ->
11 | return if @state.loading
12 | @setState(loading: true)
13 | $.getq("default", "/api/posts").always(@handleResponse).done(@handleSuccess).fail(@handleError)
14 |
15 | handleResponse: ->
16 | @setState(loading: false) if @isMounted()
17 |
18 | handleSuccess: (response) ->
19 | @setState(posts: response.posts) if @isMounted()
20 |
21 | handleError: (error) ->
22 | $.growl("Could not load posts: #{error.statusText} (#{error.status})", growlError)
23 |
24 | componentWillMount: ->
25 | @fetchPosts()
26 | @timer = setInterval(@fetchPosts, 5000)
27 | $(document).on("fetchPosts", @fetchPosts)
28 |
29 | componentWillUnmount: ->
30 | clearInterval(@timer)
31 | $(document).off("fetchPosts", @fetchPosts)
32 |
33 | render: ->
34 |
35 |
36 |
37 |
38 | {@state.posts?.map(((post) ->
39 |
40 | ).bind(this))}
41 |
42 |
43 |
51 |
52 | )
53 |
54 | @NewPostPartial = React.createClass(
55 | mixins: [ReactRouter.Navigation]
56 |
57 | propTypes:
58 | site: React.PropTypes.object.isRequired
59 |
60 | getInitialState: ->
61 | { loading: false }
62 |
63 | form: ->
64 | $(@refs.form.getDOMNode())
65 |
66 | handleSubmit: (event) ->
67 | event.preventDefault()
68 | return if @state.loading
69 |
70 | @setState(loading: true)
71 | $.postq("default", "/api/posts", @form().serialize()).always(@handleResponse).done(@handleSuccess).fail(@handleError)
72 |
73 | handleResponse: ->
74 | @setState(loading: false)
75 |
76 | handleSuccess: (response) ->
77 | @form()[0].reset()
78 | $(document).trigger("fetchPosts")
79 | @transitionTo("post_edit", post_id: response.posts.identifier)
80 |
81 | handleError: (error) ->
82 | $.growl(error.responseJSON?.errors.join(", ") || error.statusText, growlError)
83 |
84 | render: ->
85 |
99 | )
100 |
101 | @PostPartial = React.createClass(
102 | mixins: [ReactRouter.Navigation]
103 |
104 | propTypes:
105 | site: React.PropTypes.object.isRequired
106 | post: React.PropTypes.object.isRequired
107 |
108 | getInitialState: ->
109 | { loading: false }
110 |
111 | panelClass: ->
112 | if @props.post.added
113 | "success"
114 | else if @props.post.changed
115 | "warning"
116 | else if @props.post.deleted
117 | "danger"
118 | else
119 | "default"
120 |
121 | isoDate: ->
122 | try
123 | (new Date(@props.post.date.replace(/-/g, "/"))).toISOString()
124 | catch e
125 | null
126 |
127 | handleEdit: ->
128 | @transitionTo("post_edit", post_id: @props.post.identifier)
129 |
130 | handleRevert: ->
131 | return if @state.loading
132 | @setState(loading: true)
133 |
134 | $.ajaxq("default", type: "PATCH", url: "/api/posts/#{@props.post.identifier}/revert").
135 | always(@handleResponse).
136 | done(@handleSuccess).
137 | fail(@handleError)
138 |
139 | handleDelete: ->
140 | if @props.post.added
141 | return unless confirm("Are you sure? This can't be undone")
142 |
143 | return if @state.loading
144 | @setState(loading: true)
145 |
146 | $.ajaxq("default", type: "DELETE", url: "/api/posts/#{@props.post.identifier}").
147 | always(@handleResponse).
148 | done(@handleSuccess).
149 | fail(@handleError)
150 |
151 | handleRestore: ->
152 | return if @state.loading
153 | @setState(loading: true)
154 |
155 | $.ajaxq("default", type: "PATCH", url: "/api/posts/#{@props.post.identifier}/restore").
156 | always(@handleResponse).
157 | done(@handleSuccess).
158 | fail(@handleError)
159 |
160 | handleResponse: ->
161 | @setState(loading: false)
162 |
163 | handleSuccess: (response) ->
164 | $(document).trigger("fetchPosts")
165 |
166 | handleError: (error) ->
167 | $.growl("Could not load post: #{error.statusText} (#{error.status})", growlError)
168 |
169 | render: ->
170 |
171 |
172 |
{@props.post.title}
173 |
174 | {if @isoDate() then moment(@isoDate()).format("LLL") else ""}
175 |
176 |
177 |
178 |
179 |
180 |
181 | {if @props.post.deleted
182 |
183 |
184 |
185 | else
186 |
187 |
188 | {if @props.post.changed
189 |
190 | }
191 |
192 |
193 | }
194 |
195 |
196 |
197 |
198 | )
199 |
200 | @PostEdit = React.createClass(
201 | mixins: [ReactRouter.State, ReactRouter.Navigation]
202 |
203 | propTypes:
204 | site: React.PropTypes.object.isRequired
205 |
206 | getInitialState: ->
207 | { loading: false, post: null }
208 |
209 | form: ->
210 | $(@refs.form.getDOMNode())
211 |
212 | frontMatter: ->
213 | @props.site.octodmin.front_matter
214 |
215 | fetchPost: ->
216 | return if @state.loading
217 | @setState(loading: true)
218 | $.getq("default", "/api/posts/#{@getParams().post_id}").always(@handleResponse).done(@handleSuccess).fail(@handleError)
219 |
220 | handleUpload: (event)->
221 | form = $("")
222 | form.find("input").on("change", (->
223 | event.disableButtons("cmdUpload")
224 | data = new FormData(form[0])
225 | $.ajaxq("default",
226 | type: "POST"
227 | url: "/api/posts/#{@state.post.identifier}/upload"
228 | data: data
229 | cache: false
230 | contentType: false
231 | processData: false
232 | ).always(->
233 | event.enableButtons("cmdUpload")
234 | ).done(((result) ->
235 | @handleUploadSuccess(event, result)
236 | ).bind(this)).fail(@handleError)
237 | ).bind(this))
238 | form.find("input").click()
239 |
240 | handleUploadSuccess: (event, response) ->
241 | event.replaceSelection(response.uploads[0])
242 |
243 | handleBack: (event) ->
244 | event.preventDefault()
245 | @transitionTo("app")
246 |
247 | handleSubmit: (event) ->
248 | event.preventDefault()
249 | return if @state.loading
250 | @setState(loading: true)
251 |
252 | data = @form().serializeObject()
253 | $.ajaxq("default", type: "PATCH", url: "/api/posts/#{@state.post.identifier}", data: data).
254 | always(@handleResponse).
255 | done(@handleFormSuccess).
256 | fail(@handleError)
257 |
258 | handleResponse: ->
259 | @setState(loading: false)
260 |
261 | handleSuccess: (response) ->
262 | @setState(post: response.posts)
263 |
264 | handleFormSuccess: (response) ->
265 | $.growl("Post is updated", growlSuccess)
266 | @transitionTo("post_edit", post_id: response.posts.identifier)
267 |
268 | handleError: (error) ->
269 | $.growl(error.responseJSON?.errors.join(", ") || error.statusText, growlError)
270 |
271 | componentWillMount: ->
272 | @fetchPost()
273 |
274 | componentDidUpdate: ->
275 | return if !@state.post || !@refs.editor
276 | return if !/markdown$/.test(@state.post.path) && !/md$/.test(@state.post.path)
277 |
278 | $(@refs.editor.getDOMNode()).markdown(
279 | autofocus: false
280 | savable: false
281 | iconlibrary: "fa"
282 | resize: "vertical"
283 | fullscreen:
284 | enable: false
285 | additionalButtons: [
286 | [{
287 | name: "customGroup"
288 | data: [{
289 | name: "cmdUpload"
290 | title: "Upload"
291 | icon: "fa fa-upload"
292 | callback: @handleUpload
293 | }]
294 | }]
295 | ]
296 | )
297 |
298 | render: ->
299 |
300 | {if @state.post
301 |
333 | }
334 |
335 | )
336 |
--------------------------------------------------------------------------------
/spec/api/posts_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | shared_examples_for "existing post" do
4 | it "has post" do
5 | expect(subject["identifier"]).to eql("2015-01-30-test")
6 | expect(subject["added"]).to eql(false)
7 | expect(subject["changed"]).to eql(false)
8 | expect(subject["deleted"]).to eql(false)
9 | expect(subject["title"]).to eql("Test")
10 | expect(subject["slug"]).to eql("test")
11 | expect(subject["path"]).to eql("_posts/2015-01-30-test.markdown")
12 | expect(subject["date"]).to start_with("2015-01-30")
13 | expect(subject["excerpt"]).to eql("OMG
\n\n")
14 | expect(subject["content"]).to eql("# OMG\n\nThis is a *post*!\n")
15 | expect(subject["custom"]).to eql("data")
16 | end
17 | end
18 |
19 | shared_examples_for "new post" do
20 | it "has post" do
21 | expect(File.exists?("sample/_posts/#{date}-new-one.markdown")).to be_truthy
22 | expect(subject["identifier"]).to eql("#{date}-new-one")
23 | expect(subject["added"]).to eql(true)
24 | expect(subject["changed"]).to eql(false)
25 | expect(subject["deleted"]).to eql(false)
26 | expect(subject["title"]).to eql("New One")
27 | expect(subject["slug"]).to eql("new-one")
28 | expect(subject["path"]).to eql("_posts/#{date}-new-one.markdown")
29 | expect(subject["date"]).to start_with(date)
30 | expect(subject["excerpt"]).to eql("New post from template
\n\n")
31 | expect(subject["content"]).to eql("New post from **template**\n")
32 | expect(subject["custom"]).to eql("default")
33 | end
34 | end
35 |
36 | shared_examples_for "updated post" do
37 | it "has post" do
38 | expect(File.exists?("sample/_posts/2015-01-30-test.markdown")).to be_truthy
39 | expect(subject["identifier"]).to eql("2015-01-30-test")
40 | expect(subject["added"]).to eql(false)
41 | expect(subject["changed"]).to eql(true)
42 | expect(subject["deleted"]).to eql(false)
43 | expect(subject["title"]).to eql("Test")
44 | expect(subject["slug"]).to eql("test")
45 | expect(subject["path"]).to eql("_posts/2015-01-30-test.markdown")
46 | expect(subject["date"]).to start_with("2015-01-30")
47 | expect(subject["excerpt"]).to eql("WOW
\n\n")
48 | expect(subject["content"]).to eql("### WOW\n")
49 | expect(subject["custom"]).to eql("new")
50 | expect(subject.has_key?("junk")).to be_falsy
51 | end
52 | end
53 |
54 | shared_examples_for "new updated post" do
55 | it "has post" do
56 | expect(File.exists?("sample/_posts/#{date}-new-one.markdown")).to be_falsy
57 | expect(File.exists?("sample/_posts/#{date}-updated-one.markdown")).to be_truthy
58 | expect(subject["identifier"]).to eql("#{date}-updated-one")
59 | expect(subject["added"]).to eql(true)
60 | expect(subject["changed"]).to eql(false)
61 | expect(subject["deleted"]).to eql(false)
62 | expect(subject["title"]).to eql("Updated One")
63 | expect(subject["slug"]).to eql("updated-one")
64 | expect(subject["path"]).to eql("_posts/#{date}-updated-one.markdown")
65 | expect(subject["date"]).to start_with(date)
66 | expect(subject["excerpt"]).to eql("WOW
\n\n")
67 | expect(subject["content"]).to eql("### WOW\n")
68 | expect(subject["custom"]).to eql("updated")
69 | expect(subject.has_key?("junk")).to be_falsy
70 | end
71 | end
72 |
73 |
74 | shared_examples_for "deleted post" do
75 | it "has no post" do
76 | expect(File.exists?("sample/_posts/2015-01-29-welcome-to-jekyll.markdown")).to be_truthy
77 | expect(subject["identifier"]).to eql("2015-01-29-welcome-to-jekyll")
78 | expect(subject["added"]).to eql(false)
79 | expect(subject["changed"]).to eql(false)
80 | expect(subject["deleted"]).to eql(true)
81 | expect(subject["title"]).to eql("Welcome to Jekyll!")
82 | end
83 | end
84 |
85 | describe "posts" do
86 | let(:app) { Octodmin::App.new(File.expand_path("../..", __dir__)) }
87 | let(:date) { Date.today.strftime("%Y-%m-%d") }
88 |
89 | describe "index" do
90 | before { get "/api/posts" }
91 | subject { parse_json(last_response.body)["posts"] }
92 |
93 | context "first post" do
94 | subject { parse_json(last_response.body)["posts"].first }
95 | it_behaves_like "existing post"
96 | end
97 |
98 | context "last post" do
99 | specify do
100 | expect(subject.last["identifier"]).to eql("2015-01-29-welcome-to-jekyll")
101 | expect(subject.last["title"]).to eql("Welcome to Jekyll!")
102 | end
103 | end
104 | end
105 |
106 | describe "create" do
107 | context "invalid" do
108 | subject { parse_json(last_response.body)["errors"] }
109 |
110 | context "no title" do
111 | before { post "/api/posts" }
112 |
113 | it "is not ok" do
114 | expect(last_response).to_not be_ok
115 | expect(subject).to eql(["Required param `title` is not specified"])
116 | end
117 | end
118 |
119 | context "same title" do
120 | before do
121 | post "/api/posts", title: "Yo"
122 | post "/api/posts", title: "Yo"
123 | end
124 | after { File.delete("sample/_posts/#{date}-yo.markdown") }
125 |
126 | it "is not ok" do
127 | expect(last_response).to_not be_ok
128 | expect(subject).to eql(["Post with specified `title` already exists"])
129 | end
130 | end
131 | end
132 |
133 | context "valid" do
134 | context "regular post" do
135 | before { post "/api/posts", title: "New One" }
136 | after { File.delete("sample/_posts/#{date}-new-one.markdown") }
137 | subject { parse_json(last_response.body)["posts"] }
138 |
139 | context "response" do
140 | it_behaves_like "new post"
141 | end
142 |
143 | context "request" do
144 | before { get "/api/posts/#{date}-new-one" }
145 | it_behaves_like "new post"
146 | end
147 | end
148 |
149 | context "special post" do
150 | before { post "/api/posts", title: "Тестовий" }
151 | after { File.delete("sample/_posts/#{date}-testovyi.markdown") }
152 | subject { parse_json(last_response.body)["posts"] }
153 |
154 | context "response" do
155 | it "has post" do
156 | expect(File.exists?("sample/_posts/#{date}-testovyi.markdown")).to be_truthy
157 | expect(subject["identifier"]).to eql("#{date}-testovyi")
158 | end
159 | end
160 | end
161 | end
162 | end
163 |
164 | describe "show" do
165 | context "invalid" do
166 | subject { parse_json(last_response.body)["errors"] }
167 |
168 | context "no post" do
169 | before { get "/api/posts/omg" }
170 |
171 | it "is not ok" do
172 | expect(last_response).to_not be_ok
173 | expect(subject).to eql(["Could not find post"])
174 | end
175 | end
176 | end
177 |
178 | context "valid" do
179 | before { get "/api/posts/2015-01-30-test" }
180 | subject { parse_json(last_response.body)["posts"] }
181 | it_behaves_like "existing post"
182 | end
183 | end
184 |
185 | describe "update" do
186 | context "invalid" do
187 | subject { parse_json(last_response.body)["errors"] }
188 |
189 | context "no post" do
190 | before { patch "/api/posts/omg" }
191 |
192 | it "is not ok" do
193 | expect(last_response).to_not be_ok
194 | expect(subject).to eql(["Could not find post"])
195 | end
196 | end
197 |
198 | context "no params" do
199 | before { patch "/api/posts/2015-01-30-test" }
200 |
201 | it "is not ok" do
202 | expect(last_response).to_not be_ok
203 | expect(subject).to eql(["Required params are not specified"])
204 | end
205 | end
206 | end
207 |
208 | context "valid" do
209 | context "old post" do
210 | before do
211 | patch "/api/posts/2015-01-30-test", {
212 | layout: "other",
213 | title: "Test",
214 | slug: "test",
215 | date: "2015-01-30 21:10:00",
216 | content: "### WOW",
217 | custom: "new",
218 | junk: "shit",
219 | }
220 | end
221 | after do
222 | git = Git.open(Octodmin::App.dir)
223 | git.checkout("sample/_posts/2015-01-30-test.markdown")
224 | end
225 | subject { parse_json(last_response.body)["posts"] }
226 |
227 | context "response" do
228 | it_behaves_like "updated post"
229 | end
230 |
231 | context "request" do
232 | before { get "/api/posts/2015-01-30-test" }
233 | it_behaves_like "updated post"
234 | end
235 | end
236 |
237 | context "new post" do
238 | before do
239 | post "/api/posts", title: "New One"
240 | patch "/api/posts/#{date}-new-one", {
241 | layout: "post",
242 | title: "Updated One",
243 | slug: "updated-one",
244 | date: "#{date} 00:00:00",
245 | content: "### WOW",
246 | custom: "updated",
247 | junk: "shit",
248 | }
249 | end
250 | after do
251 | File.delete("sample/_posts/#{date}-updated-one.markdown")
252 | end
253 | subject { parse_json(last_response.body)["posts"] }
254 |
255 | context "response" do
256 | it_behaves_like "new updated post"
257 | end
258 |
259 | context "request" do
260 | before { get "/api/posts/#{date}-updated-one" }
261 | it_behaves_like "new updated post"
262 | end
263 | end
264 |
265 | context "special post" do
266 | before do
267 | post "/api/posts", title: "New One"
268 | patch "/api/posts/#{date}-new-one", {
269 | layout: "post",
270 | title: "Новий",
271 | slug: "novyi",
272 | date: "#{date} 00:00:00",
273 | content: "### WOW",
274 | custom: "updated",
275 | junk: "shit",
276 | }
277 | end
278 | after do
279 | File.delete("sample/_posts/#{date}-novyi.markdown")
280 | end
281 | subject { parse_json(last_response.body)["posts"] }
282 |
283 | context "response" do
284 | it "has post" do
285 | expect(File.exists?("sample/_posts/#{date}-novyi.markdown")).to be_truthy
286 | expect(subject["identifier"]).to eql("#{date}-novyi")
287 | end
288 | end
289 | end
290 | end
291 | end
292 |
293 | describe "delete" do
294 | context "invalid" do
295 | subject { parse_json(last_response.body)["errors"] }
296 |
297 | context "no post" do
298 | before { delete "/api/posts/omg" }
299 |
300 | it "is not ok" do
301 | expect(last_response).to_not be_ok
302 | expect(subject).to eql(["Could not find post"])
303 | end
304 | end
305 | end
306 |
307 | context "valid" do
308 | context "new post" do
309 | before do
310 | post "/api/posts", title: "New One"
311 | delete "/api/posts/#{date}-new-one"
312 | get "/api/posts/#{date}-new-one"
313 | end
314 | subject { parse_json(last_response.body)["errors"] }
315 |
316 | it "has no post" do
317 | expect(last_response).to_not be_ok
318 | expect(subject).to eql(["Could not find post"])
319 | end
320 | end
321 |
322 | context "old post" do
323 | before do
324 | delete "/api/posts/2015-01-29-welcome-to-jekyll"
325 | end
326 | after do
327 | git = Git.open(Octodmin::App.dir)
328 | git.add("sample/_posts/2015-01-29-welcome-to-jekyll.markdown")
329 | end
330 | subject { parse_json(last_response.body)["posts"] }
331 | it_behaves_like "deleted post"
332 | end
333 | end
334 | end
335 |
336 | describe "restore" do
337 | context "invalid" do
338 | subject { parse_json(last_response.body)["errors"] }
339 |
340 | context "no post" do
341 | before { patch "/api/posts/omg/restore" }
342 |
343 | it "is not ok" do
344 | expect(last_response).to_not be_ok
345 | expect(subject).to eql(["Could not find post"])
346 | end
347 | end
348 | end
349 |
350 | context "valid" do
351 | context "existing post" do
352 | before do
353 | patch "/api/posts/2015-01-30-test/restore"
354 | end
355 | subject { parse_json(last_response.body)["posts"] }
356 | it_behaves_like "existing post"
357 | end
358 |
359 | context "deleted post" do
360 | before do
361 | delete "/api/posts/2015-01-30-test"
362 | patch "/api/posts/2015-01-30-test/restore"
363 | end
364 | subject { parse_json(last_response.body)["posts"] }
365 | it_behaves_like "existing post"
366 | end
367 | end
368 | end
369 |
370 | describe "revert" do
371 | context "invalid" do
372 | subject { parse_json(last_response.body)["errors"] }
373 |
374 | context "no post" do
375 | before { patch "/api/posts/omg/revert" }
376 |
377 | it "is not ok" do
378 | expect(last_response).to_not be_ok
379 | expect(subject).to eql(["Could not find post"])
380 | end
381 | end
382 | end
383 |
384 | context "valid" do
385 | context "existing post" do
386 | before do
387 | patch "/api/posts/2015-01-30-test/revert"
388 | end
389 | subject { parse_json(last_response.body)["posts"] }
390 | it_behaves_like "existing post"
391 | end
392 |
393 | context "changed post" do
394 | before do
395 | patch "/api/posts/2015-01-30-test", {
396 | layout: "other",
397 | title: "Test",
398 | slug: "test",
399 | date: "2015-01-30 21:10:00",
400 | content: "### WOW",
401 | custom: "new",
402 | junk: "shit",
403 | }
404 | patch "/api/posts/2015-01-30-test/revert"
405 | end
406 | subject { parse_json(last_response.body)["posts"] }
407 | it_behaves_like "existing post"
408 | end
409 | end
410 | end
411 |
412 | describe "upload" do
413 | context "valid" do
414 | before do
415 | post "/api/posts/2015-01-30-test/upload", {
416 | file: Rack::Test::UploadedFile.new("spec/fixtures/ear.png")
417 | }
418 | end
419 | after do
420 | File.delete("sample/octodmin/2015-01-30-test/ear.png")
421 | end
422 | subject { parse_json(last_response.body)["posts"] }
423 |
424 | specify do
425 | expect(File.exists?("sample/octodmin/2015-01-30-test/ear.png")).to be_truthy
426 | end
427 | end
428 | end
429 | end
430 |
--------------------------------------------------------------------------------