├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ └── plotline │ │ │ ├── application.js │ │ │ ├── behaviors │ │ │ ├── alerts.js │ │ │ └── dropdown.js │ │ │ ├── photosets.js │ │ │ ├── plotline-utils.js │ │ │ └── plotline.js │ └── stylesheets │ │ └── plotline │ │ ├── application.scss │ │ ├── components │ │ ├── base.scss │ │ ├── buttons.scss │ │ ├── common.scss │ │ ├── dropdowns.scss │ │ ├── flashes.scss │ │ ├── forms.scss │ │ ├── helpers.scss │ │ ├── normalize.scss │ │ ├── tables.scss │ │ ├── typography.scss │ │ └── variables.scss │ │ ├── navigation.scss │ │ ├── photosets.scss │ │ └── shared.scss ├── controllers │ └── plotline │ │ ├── application_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── entries_controller.rb │ │ ├── images_controller.rb │ │ └── sessions_controller.rb ├── helpers │ └── plotline │ │ └── application_helper.rb ├── models │ └── plotline │ │ ├── concerns │ │ ├── family.rb │ │ ├── searchable.rb │ │ └── taggable.rb │ │ ├── entry.rb │ │ ├── entry_search_data.rb │ │ ├── image.rb │ │ └── user.rb ├── presenters │ └── plotline │ │ ├── base_presenter.rb │ │ └── entry_presenter.rb └── views │ ├── layouts │ └── plotline │ │ ├── application.html.erb │ │ └── plain.html.erb │ └── plotline │ ├── dashboard │ └── index.html.erb │ ├── entries │ ├── index.html.erb │ └── show.html.erb │ ├── images │ ├── index.html.erb │ └── show.html.erb │ ├── sessions │ └── new.html.erb │ └── shared │ ├── _flash_messages.html.erb │ └── _navbar.html.erb ├── bin ├── plotline └── rails ├── config └── routes.rb ├── db └── migrate │ ├── 20150831131759_create_plotline_entries.plotline.rb │ ├── 20150911135536_create_plotline_entry_search_data.plotline.rb │ ├── 20160212172219_create_plotline_images.plotline.rb │ └── 20160224161843_create_plotline_users.plotline.rb ├── lib ├── plotline.rb ├── plotline │ ├── configuration.rb │ ├── custom_markdown_parser.rb │ ├── engine.rb │ ├── import │ │ ├── handlers │ │ │ ├── base.rb │ │ │ ├── image_file.rb │ │ │ ├── markdown_file.rb │ │ │ └── video_file.rb │ │ └── runner.rb │ └── version.rb └── tasks │ └── plotline_tasks.rake ├── plotline.gemspec └── test ├── controllers └── plotline │ ├── dashboard_controller_test.rb │ └── entries_controller_test.rb ├── dummy ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ └── concerns │ │ │ └── .keep │ └── views │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico ├── fixtures ├── markdown │ ├── document.md │ └── expected_output.html └── plotline │ └── entries.yml ├── integration └── navigation_test.rb ├── models └── plotline │ ├── concerns │ └── searchable_test.rb │ ├── entry_search_data_test.rb │ ├── entry_test.rb │ └── user_test.rb ├── plotline_test.rb ├── presenters └── entry_presenter_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in plotline.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | 16 | gem 'rails-controller-testing' 17 | gem 'mocha' 18 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | plotline (0.1.1) 5 | autoprefixer-rails 6 | bcrypt (~> 3.1.7) 7 | bourbon 8 | commander 9 | exiftool 10 | fastimage 11 | jquery-rails 12 | listen 13 | rails (~> 5.0.0.1) 14 | rdiscount 15 | sass-rails (~> 5.0) 16 | 17 | GEM 18 | remote: https://rubygems.org/ 19 | specs: 20 | actioncable (5.0.0.1) 21 | actionpack (= 5.0.0.1) 22 | nio4r (~> 1.2) 23 | websocket-driver (~> 0.6.1) 24 | actionmailer (5.0.0.1) 25 | actionpack (= 5.0.0.1) 26 | actionview (= 5.0.0.1) 27 | activejob (= 5.0.0.1) 28 | mail (~> 2.5, >= 2.5.4) 29 | rails-dom-testing (~> 2.0) 30 | actionpack (5.0.0.1) 31 | actionview (= 5.0.0.1) 32 | activesupport (= 5.0.0.1) 33 | rack (~> 2.0) 34 | rack-test (~> 0.6.3) 35 | rails-dom-testing (~> 2.0) 36 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 37 | actionview (5.0.0.1) 38 | activesupport (= 5.0.0.1) 39 | builder (~> 3.1) 40 | erubis (~> 2.7.0) 41 | rails-dom-testing (~> 2.0) 42 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 43 | activejob (5.0.0.1) 44 | activesupport (= 5.0.0.1) 45 | globalid (>= 0.3.6) 46 | activemodel (5.0.0.1) 47 | activesupport (= 5.0.0.1) 48 | activerecord (5.0.0.1) 49 | activemodel (= 5.0.0.1) 50 | activesupport (= 5.0.0.1) 51 | arel (~> 7.0) 52 | activesupport (5.0.0.1) 53 | concurrent-ruby (~> 1.0, >= 1.0.2) 54 | i18n (~> 0.7) 55 | minitest (~> 5.1) 56 | tzinfo (~> 1.1) 57 | addressable (2.4.0) 58 | arel (7.1.1) 59 | autoprefixer-rails (6.4.0.2) 60 | execjs 61 | bcrypt (3.1.11) 62 | bourbon (4.2.7) 63 | sass (~> 3.4) 64 | thor (~> 0.19) 65 | builder (3.2.2) 66 | commander (4.4.0) 67 | highline (~> 1.7.2) 68 | concurrent-ruby (1.0.2) 69 | erubis (2.7.0) 70 | execjs (2.7.0) 71 | exiftool (0.7.0) 72 | json 73 | fastimage (2.0.0) 74 | addressable (~> 2) 75 | ffi (1.9.14) 76 | globalid (0.3.7) 77 | activesupport (>= 4.1.0) 78 | highline (1.7.8) 79 | i18n (0.7.0) 80 | jquery-rails (4.1.1) 81 | rails-dom-testing (>= 1, < 3) 82 | railties (>= 4.2.0) 83 | thor (>= 0.14, < 2.0) 84 | json (2.0.2) 85 | listen (3.1.5) 86 | rb-fsevent (~> 0.9, >= 0.9.4) 87 | rb-inotify (~> 0.9, >= 0.9.7) 88 | ruby_dep (~> 1.2) 89 | loofah (2.0.3) 90 | nokogiri (>= 1.5.9) 91 | mail (2.6.4) 92 | mime-types (>= 1.16, < 4) 93 | metaclass (0.0.4) 94 | method_source (0.8.2) 95 | mime-types (3.1) 96 | mime-types-data (~> 3.2015) 97 | mime-types-data (3.2016.0521) 98 | mini_portile2 (2.1.0) 99 | minitest (5.9.0) 100 | mocha (1.1.0) 101 | metaclass (~> 0.0.1) 102 | nio4r (1.2.1) 103 | nokogiri (1.6.8) 104 | mini_portile2 (~> 2.1.0) 105 | pkg-config (~> 1.1.7) 106 | pg (0.18.2) 107 | pkg-config (1.1.7) 108 | rack (2.0.1) 109 | rack-test (0.6.3) 110 | rack (>= 1.0) 111 | rails (5.0.0.1) 112 | actioncable (= 5.0.0.1) 113 | actionmailer (= 5.0.0.1) 114 | actionpack (= 5.0.0.1) 115 | actionview (= 5.0.0.1) 116 | activejob (= 5.0.0.1) 117 | activemodel (= 5.0.0.1) 118 | activerecord (= 5.0.0.1) 119 | activesupport (= 5.0.0.1) 120 | bundler (>= 1.3.0, < 2.0) 121 | railties (= 5.0.0.1) 122 | sprockets-rails (>= 2.0.0) 123 | rails-controller-testing (0.1.1) 124 | actionpack (~> 5.x) 125 | actionview (~> 5.x) 126 | activesupport (~> 5.x) 127 | rails-dom-testing (2.0.1) 128 | activesupport (>= 4.2.0, < 6.0) 129 | nokogiri (~> 1.6.0) 130 | rails-html-sanitizer (1.0.3) 131 | loofah (~> 2.0) 132 | railties (5.0.0.1) 133 | actionpack (= 5.0.0.1) 134 | activesupport (= 5.0.0.1) 135 | method_source 136 | rake (>= 0.8.7) 137 | thor (>= 0.18.1, < 2.0) 138 | rake (11.2.2) 139 | rb-fsevent (0.9.7) 140 | rb-inotify (0.9.7) 141 | ffi (>= 0.5.0) 142 | rdiscount (2.2.0.1) 143 | ruby_dep (1.4.0) 144 | sass (3.4.22) 145 | sass-rails (5.0.6) 146 | railties (>= 4.0.0, < 6) 147 | sass (~> 3.1) 148 | sprockets (>= 2.8, < 4.0) 149 | sprockets-rails (>= 2.0, < 4.0) 150 | tilt (>= 1.1, < 3) 151 | sprockets (3.7.0) 152 | concurrent-ruby (~> 1.0) 153 | rack (> 1, < 3) 154 | sprockets-rails (3.1.1) 155 | actionpack (>= 4.0) 156 | activesupport (>= 4.0) 157 | sprockets (>= 3.0.0) 158 | thor (0.19.1) 159 | thread_safe (0.3.5) 160 | tilt (2.0.5) 161 | tzinfo (1.2.2) 162 | thread_safe (~> 0.1) 163 | websocket-driver (0.6.4) 164 | websocket-extensions (>= 0.1.0) 165 | websocket-extensions (0.1.2) 166 | 167 | PLATFORMS 168 | ruby 169 | 170 | DEPENDENCIES 171 | mocha 172 | pg 173 | plotline! 174 | rails-controller-testing 175 | 176 | BUNDLED WITH 177 | 1.12.5 178 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Piotr Chmolowski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plotline 2 | 3 | Plotline is a flexible CMS engine for Rails apps, based on Postgres, Markdown 4 | and Dropbox. It provides the essentials you need to create any content website: 5 | 6 | * Data model for storing your content 7 | * Full-text search engine (Postgres-based) 8 | * Markdown parser with additional features (responsive photosets, custom attributes) 9 | * YAML Front Matter for metadata 10 | * Tags 11 | * Images stored with metadata (exif, width, height) 12 | * Dropbox sync for content files 13 | 14 | It comes with a (very) basic admin panel, but it doesn't include any forms 15 | for creating and editing data (yet). 16 | 17 | ## Getting Started 18 | 19 | Best way to get started is to take a look at the 20 | [demo blog app](https://github.com/pch/plotline-demo-blog). Clone the repo, run 21 | the app and make it your own. 22 | 23 | For a real-world example of what you can do with Plotline, head over to 24 | [my personal website](http://pchm.co). 25 | 26 | * * * 27 | 28 | # Usage 29 | 30 | Plotline can be used as a part of any Postgres-based Rails app: 31 | 32 | ```shell 33 | rails new blog --database=postgresql 34 | ``` 35 | 36 | Add Plotline to the Gemfile: 37 | 38 | ```ruby 39 | gem 'plotline' 40 | ``` 41 | 42 | Run `bundle install` and set up database migrations: 43 | 44 | ``` 45 | rake plotline:install:migrations 46 | rake db:migrate 47 | ``` 48 | 49 | ## Content Classes 50 | 51 | In order to make use of the CMS engine, you'll need to create a content class 52 | for each of your content type, for example: 53 | 54 | ```ruby 55 | class BlogPost < Plotline::Entry 56 | # All classes have "title" and "body" columns, everything else is a content_attr: 57 | content_attr :subtitle 58 | content_attr :cover_image 59 | content_attr :author_name 60 | 61 | searchable_attributes :title, :body 62 | end 63 | ``` 64 | 65 | `Plotline::Entry` serves as a base for all your custom content. It is a regular 66 | ActiveRecord model with additional features, like dynamic content attributes 67 | (without having to write database migrations), full-text search and tags. 68 | 69 | * * * 70 | 71 | # Features 72 | 73 | ## Content vs Application Code 74 | 75 | Plotline is **not** a static site generator. Plotline-based website is a regular 76 | Ruby on Rails & Postgres application. However, it allows you to store your 77 | Markdown files and images in a Dropbox directory. Any changes you make to that 78 | folder will be imported to the database within seconds. 79 | 80 | You can write your content in Markdown using your favorite editor and still get 81 | the benefits and flexibility of a dynamic web application. 82 | 83 | ### Sync 84 | 85 | Dropbox is just a recommended (and tested) method, but Plotline doesn't make any 86 | assumptions regarding the underlying sync technology. It simply monitors the 87 | given source directory for changes and imports them to the database: 88 | 89 | ```shell 90 | bundle exec plotline sync --source /Users/janedoe/Dropbox/blog 91 | ``` 92 | 93 | **Note:** If you want to use Dropbox sync, keep in mind that **all your files 94 | from the Dropbox directory will be downloaded to the server**. To avoid this, it 95 | is recommended to sign up for a fresh Dropbox account, create a new directory 96 | for your content and share it with your main account. This way, you can link 97 | the app server to the new account and only the content directory will be 98 | downloaded. 99 | 100 | ## Content Directory Structure 101 | 102 | Plotline doesn't enforce a specific structure within the content directory, so 103 | you can arrange files any way you like. Example directory can look like this: 104 | 105 | ``` 106 | media/ 107 | 2016/ 108 | hello-world/ 109 | hello.jpg 110 | 111 | drafts/ 112 | blog-post-idea.md 113 | 114 | blog-posts/ 115 | 2016/ 116 | 2016-03-25-hello-world.md 117 | ``` 118 | 119 | ## Markdown Files Conventions 120 | 121 | Markdown files should be named in the following way: 122 | 123 | ``` 124 | YYYY-MM-DD-slug.md 125 | ``` 126 | 127 | The date indicates when the post will be published (future dates are allowed). 128 | The `slug` part will be the url of the post. 129 | 130 | Files without a `published_at` date and with a `draft: true` attribute in the 131 | front matter sections are considered drafts: 132 | 133 | ```markdown 134 | --- 135 | title: "Hello World" 136 | type: post 137 | draft: true 138 | --- 139 | 140 | This is a draft of my new blog post. 141 | ``` 142 | 143 | You can fetch drafts with the following code: 144 | 145 | ```ruby 146 | @drafts = BlogPost.drafts.order('id DESC') 147 | ``` 148 | 149 | Similarly, published entries should be fetched using the `published` scope: 150 | 151 | ``` 152 | @posts = BlogPost.published.order('published_at DESC') 153 | ``` 154 | 155 | ### YAML Front Matter 156 | 157 | YAML Front Matter is a special part at the beginning of a Markdown file where 158 | you define metadata attributes: 159 | 160 | ```markdown 161 | --- 162 | title: "Hello World" 163 | type: post 164 | tags: 165 | - tutorial 166 | - guide 167 | --- 168 | 169 | Here goes the actual content. 170 | ``` 171 | 172 | The `title` and `type` attributes are required. Everything else is optional, 173 | but anything you put here should map to a `content_attr` in your content class. 174 | 175 | ## Custom Markdown 176 | 177 | Plotline uses RDiscount as its underlying markdown parser. However, it provides 178 | its own pre-processor for images. It means that Markdown tags like 179 | `![](http://example.com/image.jpg)` will not be parsed by RDicount, but by 180 | plotline's `CustomMarkdownProcessor`. 181 | 182 | The idea behind it is to extend basic Markdown images with additional options, 183 | like custom attributes, and responsive photosets (galleries/grids). 184 | 185 | For example: 186 | 187 | ```markdown 188 | ![Example image](http://example.com/image.jpg){.center .big #main-img data-behavior=responsive} 189 | ``` 190 | 191 | ..will result in the following markup: 192 | 193 | ```html 194 |
195 | 196 |
Example image
197 |
198 | ``` 199 | 200 | ### Responsive Photosets 201 | 202 | Plotline provides an additional, custom Markdown syntax to allow you to create 203 | responsive image galleries. Text enclosed in `--- ... ---` will be parsed as 204 | a photoset block: 205 | 206 | ```markdown 207 | Here are the photos from my last trip: 208 | 209 | --- 210 | ![](../../media/posts/hello/photo1.jpg) 211 | ![](../../media/posts/hello/photo2.jpg) 212 | 213 | ![](../../media/posts/hello/photo3.jpg) 214 | 215 | ![](../../media/posts/hello/photo4.jpg) 216 | ![](../../media/posts/hello/photo5.jpg) 217 | ![](../../media/posts/hello/photo5.jpg) 218 | --- 219 | 220 | Continues here. 221 | ``` 222 | 223 | From the example above, Plotline will create a photoset with 3 rows: 2, 1 and 3 224 | images in each row respectively. Empty line marks a new row. 225 | 226 | Relative image paths will be replaced and expanded during sync to the database, 227 | so you don't have to worry about keeping a specific directory structure. 228 | 229 | ## Example Document 230 | 231 | ```markdown 232 | --- 233 | title: "The Adventures of Tom Sawyer" 234 | author: "Mark Twain" 235 | type: post 236 | cover_image: "../../media/hello-world.jpg" 237 | tags: 238 | - books 239 | - mark twain 240 | - adventure 241 | --- 242 | 243 | ![](../../media/posts/tom-sawyer/illustration1.jpg){.center #main-img data-behavior=responsive} 244 | 245 | Within two minutes, or even less, he had forgotten all his troubles. Not because 246 | his troubles were one whit less heavy and bitter to him than a man's are to 247 | a man, but because a new and powerful interest bore them down and drove them out 248 | of his mind for the time — just as men's misfortunes are forgotten in the 249 | excitement of new enterprises. This new interest was a valued novelty in 250 | whistling, which he had just acquired from a negro, and he was suffering 251 | to practise it undisturbed. It con- sisted in a peculiar bird-like turn, a sort 252 | of liquid warble, pro- duced by touching the tongue to the roof of the mouth at 253 | short intervals in the midst of the music — the reader probably re- members how 254 | to do it, if he has ever been a boy. 255 | 256 | --- 257 | ![](../../media/posts/tom-sawyer/illustration2.jpg) 258 | ![](../../media/posts/tom-sawyer/illustration3.jpg) 259 | 260 | ![](../../media/posts/tom-sawyer/illustration4.jpg) 261 | 262 | ![](../../media/posts/tom-sawyer/illustration5.jpg) 263 | ![](../../media/posts/tom-sawyer/illustration6.jpg) 264 | --- 265 | 266 | Diligence and attention soon gave him the 267 | knack of it, and he strode down the street with his mouth full of harmony and 268 | his soul full of gratitude. He felt much as an astronomer feels who has 269 | discovered a new planet — no doubt, as far as strong, deep, unal- loyed pleasure 270 | is concerned, the advantage was with the boy, not the astronomer. 271 | ``` 272 | 273 | * * * 274 | 275 | 276 | ## Parent-child Associations 277 | 278 | Content classes can be linked in a parent-child associations: 279 | 280 | ```ruby 281 | class BlogPost < Plotline::Entry 282 | child_entries :comments 283 | end 284 | 285 | class Comment < Plotline::Entry 286 | parent_entry :blog_post 287 | end 288 | 289 | post = BlogPost.new 290 | post.comments # is the same as: 291 | post.children 292 | 293 | comment = Comment.new 294 | comment.blog_post # is the same as: 295 | comment.parent 296 | ``` 297 | 298 | `child_entries` and `parent_entry` methods are merely convinience aliases to 299 | the `has_many :children` and `belongs_to :parent` associations. 300 | 301 | ## Full-text Search 302 | 303 | Plotline comes with a simple Postgres-based full-text search engine. Any content 304 | attribute you list in `searchable_attrs` will be indexed to the `search_data` 305 | table `after_save`: 306 | 307 | ```ruby 308 | class BlogPost < Plotline::Entry 309 | searchable_attrs :title, :body 310 | end 311 | 312 | BlogPost.published.search("Hello World") 313 | ``` 314 | 315 | ## Tags 316 | 317 | Tags are stored in the database as Postgres arrays: 318 | 319 | ```ruby 320 | BlogPost.create(title: "Hello World", tags: ['intro', 'writing'], published_at: Time.now) 321 | BlogPost.published.tagged_with("writing") 322 | ``` 323 | 324 | ## Deployment 325 | 326 | Rails apps can be a little tedious to deploy, so Plotline provides an example 327 | [Ansible Playbook](https://github.com/pch/plotline-ansible) you can use to 328 | easily set up a server from scratch. 329 | 330 | It should run on any Ubuntu 14.04 VPS. Heroku won't work with the 331 | built-in Dropbox sync, due to platform limitations. 332 | 333 | * * * 334 | 335 | ## Known Issues / Limitations 336 | 337 | * Image Markdown tags accept only paths/urls to files, the 338 | `![Caption][image-label]` syntax is not supported. 339 | * For some reason, `
` tags are wrapped in `

` tags by RDiscount, 340 | which may not always be acceptable. 341 | * External images (referenced by URLs) won't store Exif data in the database. 342 | It should be fairly easy to fix by downloading those images to a temp file 343 | and passing them to Exiftool. However, I don't use external images, so it 344 | wasn't a priority to implement. I will happily accept a PR, though. 345 | 346 | ## License 347 | 348 | Plotline is released under the [MIT License](http://www.opensource.org/licenses/MIT). 349 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Plotline' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | require 'rake/testtask' 28 | 29 | Rake::TestTask.new(:test) do |t| 30 | t.libs << 'lib' 31 | t.libs << 'test' 32 | t.pattern = 'test/**/*_test.rb' 33 | t.verbose = false 34 | end 35 | 36 | 37 | task default: :test 38 | -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | // 4 | //= require ./plotline 5 | //= require ./plotline-utils 6 | //= require ./photosets 7 | //= 8 | //= require ./behaviors/alerts 9 | //= require ./behaviors/dropdown 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/behaviors/alerts.js: -------------------------------------------------------------------------------- 1 | Plotline.Behaviors.Alert = { 2 | closeBtnClass: '.alert-close', 3 | 4 | initialize: function() { 5 | $(this.closeBtnClass).bind('click', (function(_this) { 6 | return function(event) { 7 | $(event.target).parent().hide(); 8 | return event.preventDefault(); 9 | }; 10 | })(this)); 11 | 12 | return setTimeout((function(_this) { 13 | return function() { 14 | return $(".flash").fadeOut(1000); 15 | }; 16 | })(this), 3000); 17 | } 18 | } 19 | 20 | Plotline.Behaviors.Alert.initialize(); 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/behaviors/dropdown.js: -------------------------------------------------------------------------------- 1 | Plotline.Behaviors.Dropdown = { 2 | initialize: function() { 3 | $(document).ready(function() { 4 | $('[data-behavior~=dropdown] .nav-item').click(function(e) { 5 | $(this).siblings("ul").toggleClass("visible"); 6 | e.preventDefault(); 7 | }); 8 | }); 9 | 10 | $(window).click(function(event) { 11 | if (!$('[data-behavior~=dropdown]').has(event.target).length) { 12 | $('[data-behavior~=dropdown] ul').removeClass("visible"); 13 | } 14 | }); 15 | } 16 | } 17 | 18 | Plotline.Behaviors.Dropdown.initialize(); -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/photosets.js: -------------------------------------------------------------------------------- 1 | var Plotline = Plotline || {}; 2 | 3 | // Responsive Photosets 4 | // 5 | // Based heavily on/stolen from: https://medium.com/coding-design/responsive-photosets-7742e6f93d9e 6 | Plotline.Photosets = { 7 | initialize: function() { 8 | window.addEventListener('resize', Plotline.Utils.debounce(this.align.bind(this), 100)); 9 | 10 | Plotline.Utils.ready(function() { 11 | // trigger 'resize' event on window 12 | var event = document.createEvent('HTMLEvents'); 13 | event.initEvent('resize', true, false); 14 | window.dispatchEvent(event); 15 | }); 16 | }, 17 | 18 | align: function(event) { 19 | var rows = document.querySelectorAll('.photoset-row'); 20 | Array.prototype.forEach.call(rows, function(el, i){ 21 | var $pi = el.querySelectorAll('.photoset-item'), 22 | cWidth = parseInt(window.getComputedStyle(el.parentNode).width); 23 | 24 | var ratios = Array.prototype.map.call($pi, function(el, index) { 25 | return el.querySelector('img').getAttribute('data-ratio'); 26 | }); 27 | 28 | var sumRatios = 0, sumMargins = 0, 29 | minRatio = Math.min.apply(Math, ratios); 30 | 31 | for (var i = 0; i < $pi.length; i++) { 32 | sumRatios += ratios[i] / minRatio; 33 | }; 34 | 35 | Array.prototype.forEach.call($pi, function(el) { 36 | sumMargins += parseInt(getComputedStyle(el)['margin-left']) + parseInt(getComputedStyle(el)['margin-right']); 37 | }); 38 | 39 | Array.prototype.forEach.call($pi, function(el, i) { 40 | var minWidth = (cWidth - sumMargins) / sumRatios; 41 | var img = el.querySelector('img'); 42 | 43 | img.style.width = (Math.ceil(minWidth / minRatio) * ratios[i]) + 'px'; 44 | img.style.height = (Math.ceil(minWidth / minRatio)) + 'px'; 45 | }); 46 | }); 47 | } 48 | }; 49 | 50 | Plotline.Photosets.initialize(); 51 | -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/plotline-utils.js: -------------------------------------------------------------------------------- 1 | var Plotline = Plotline || {}; 2 | Plotline.Utils = { 3 | ready: function(callback) { 4 | if (document.readyState != 'loading'){ 5 | callback(); 6 | } else { 7 | document.addEventListener('DOMContentLoaded', callback); 8 | } 9 | }, 10 | 11 | // Source: https://remysharp.com/2010/07/21/throttling-function-calls 12 | debounce: function(fn, delay) { 13 | var timer = null; 14 | return function () { 15 | var context = this, args = arguments; 16 | clearTimeout(timer); 17 | timer = setTimeout(function () { 18 | fn.apply(context, args); 19 | }, delay); 20 | }; 21 | }, 22 | 23 | toggleClass: function(el, className) { 24 | if (el.classList) { 25 | el.classList.toggle(className); 26 | } else { 27 | var classes = el.className.split(' '); 28 | var existingIndex = classes.indexOf(className); 29 | 30 | if (existingIndex >= 0) 31 | classes.splice(existingIndex, 1); 32 | else 33 | classes.push(className); 34 | 35 | el.className = classes.join(' '); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/assets/javascripts/plotline/plotline.js: -------------------------------------------------------------------------------- 1 | var Plotline = Plotline || {}; 2 | 3 | Plotline.Behaviors = Plotline.Behaviors || {}; 4 | Plotline.initialize = function() {}; 5 | Plotline.initialize(); 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/application.scss: -------------------------------------------------------------------------------- 1 | @import 'components/variables'; 2 | 3 | $em-base: 14px; 4 | 5 | // Colors 6 | $blue: #328ad6; 7 | $dark-gray: #333; 8 | $medium-gray: #999; 9 | $light-gray: #DDD; 10 | $light-red: #FBE3E4; 11 | $light-yellow: #FFF6BF; 12 | $light-green: #8aba56; 13 | $yellow: #FDBD41; 14 | $red: #FC625D; 15 | $green: #34C849; 16 | $blue-gray: #F2F5F8; 17 | 18 | $base-background-color: #fff; 19 | $base-font-color: #262626; 20 | $base-font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 21 | $form-font-family: "Fira Mono", Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 22 | 23 | $muted-text-color: #82888a; 24 | 25 | $base-font-size: $em-base; 26 | $base-line-height: 1.43; 27 | 28 | $base-border-color: #d4d4d4; 29 | 30 | // Links 31 | $base-link-color: $blue; 32 | $hover-link-color: darken($base-link-color, 20); 33 | 34 | // Navigation 35 | $nav-background-color: #fafafa; 36 | $nav-link-color: #262626; 37 | $nav-link-color-active: $blue; 38 | //$nav-link-color-active: #5b7a59; 39 | $nav-active-highlight-color: #cecfd1; 40 | 41 | $alert-color: $light-yellow; 42 | $error-color: $light-red; 43 | $notice-color: lighten(#12A5F4, 40); 44 | $success-color: $light-green; 45 | 46 | $nav-height: 55px; 47 | $form-title-height: 70px; 48 | $bottom-toolbar-height: 40px; 49 | 50 | @import "bourbon"; 51 | @import "components/base"; 52 | 53 | // Application-specific 54 | @import "shared"; 55 | @import "navigation"; 56 | @import "photosets"; 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/base.scss: -------------------------------------------------------------------------------- 1 | // Settings 2 | @import 'settings/prefixer'; 3 | @import 'settings/px-to-em'; 4 | 5 | @import '../components/normalize'; 6 | @import '../components/helpers'; 7 | @import '../components/typography'; 8 | @import '../components/common'; 9 | @import '../components/forms'; 10 | @import '../components/tables'; 11 | @import '../components/buttons'; 12 | @import '../components/dropdowns'; 13 | @import '../components/flashes'; 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/buttons.scss: -------------------------------------------------------------------------------- 1 | 2 | .btn { 3 | display: inline-block; 4 | margin-bottom: 0; 5 | text-align: center; 6 | vertical-align: middle; 7 | line-height: 1; 8 | white-space: nowrap; 9 | cursor: pointer; 10 | position: relative; 11 | 12 | border-radius: 5px; 13 | border: none; 14 | color: #fff; 15 | font-size: 16px; 16 | font-weight: bold; 17 | padding: 10px 21px; 18 | 19 | @include clear-user-select; 20 | } 21 | 22 | .btn-primary.btn { 23 | //background-image: linear-gradient(-1deg, #3494F7 0%, #40ABF9 100%); 24 | background-color: #328ad6; 25 | box-shadow: inset 0 0 0 1px #328ad6; 26 | } 27 | 28 | .btn:hover, .btn:focus { text-decoration: none } 29 | .btn:hover, .btn:focus { color:#fff } 30 | 31 | .btn-primary.btn:hover, .btn-primary.btn:focus { 32 | background-color: li ghten(#328ad6, 5); 33 | color:#fff 34 | } 35 | 36 | .btn-primary.btn:active { 37 | background-color: lighten(#3494F7, 8); 38 | } 39 | 40 | .btn.full-width { 41 | width: 100%; 42 | } 43 | 44 | .btn.btn-secondary { 45 | border: none; 46 | background: #34C849; 47 | color: #fff; 48 | } 49 | 50 | .btn.btn-secondary:hover { 51 | background: darken(#34C849, 5); 52 | } 53 | 54 | .btn.btn-secondary:active { 55 | background: darken(#34C849, 10); 56 | } 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/common.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after, hr, hr:before, hr:after, input[type="search"], input[type="search"]:before, input[type="search"]:after { 2 | -moz-box-sizing: border-box; 3 | box-sizing: border-box; 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/dropdowns.scss: -------------------------------------------------------------------------------- 1 | 2 | .dropdown { 3 | position: relative; 4 | } 5 | 6 | .dropdown > a { 7 | display: block; 8 | } 9 | 10 | .dropdown > a .caret { 11 | font-size: 10px; 12 | } 13 | 14 | .dropdown ul { 15 | background: $nav-background-color; 16 | padding: 1px; 17 | position: absolute; 18 | left: 0px; 19 | top: $nav-height; 20 | width: 200px; 21 | box-shadow: 0px 0px 5px 0px rgba(56,70,78,0.33); 22 | display: none; 23 | opacity: 0; 24 | visibility: hidden; 25 | z-index: 999; 26 | 27 | border-bottom-color: rgb(119, 119, 119); 28 | border-bottom-left-radius: 3px; 29 | border-bottom-right-radius: 3px; 30 | border-bottom-style: none; 31 | border-bottom-width: 0px; 32 | border-left-color: rgb(119, 119, 119); 33 | border-left-style: none; 34 | border-left-width: 0px; 35 | border-right-color: rgb(119, 119, 119); 36 | border-right-style: none; 37 | border-right-width: 0px; 38 | border-top-color: rgb(119, 119, 119); 39 | border-top-left-radius: 0px; 40 | border-top-right-radius: 0px; 41 | border-top-style: none; 42 | border-top-width: 0px; 43 | } 44 | 45 | .dropdown ul li { 46 | display: block; 47 | color: #fff; 48 | } 49 | 50 | .dropdown ul li.separate { 51 | margin: 4px 0 0 0; 52 | padding: 4px 0 0 0; 53 | border-top: 1px solid rgba(255,255,255,0.15); 54 | } 55 | 56 | .dropdown ul li a { 57 | display: block; 58 | padding: 12px; 59 | } 60 | 61 | 62 | .dropdown ul.visible { 63 | display: block; 64 | opacity: 1; 65 | visibility: visible; 66 | } 67 | 68 | .dropdown a { 69 | color: $nav-link-color; 70 | } 71 | 72 | .dropdown a:hover { 73 | color: $nav-link-color-active; 74 | } 75 | 76 | .dropdown ul li a:hover { 77 | color: #fff; 78 | background: $blue; 79 | } 80 | 81 | .dropdown ul li a:active { 82 | color: #fff; 83 | background: $blue; 84 | } 85 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/flashes.scss: -------------------------------------------------------------------------------- 1 | @mixin flash($color) { 2 | background: $color; 3 | box-shadow: darken($color, 20) 0px 1px 1px 0px; 4 | 5 | color: darken($color, 60); 6 | 7 | a { 8 | color: darken($color, 70); 9 | 10 | &:hover { 11 | color: darken($color, 90); 12 | } 13 | 14 | float: right; 15 | } 16 | } 17 | 18 | %flash-base { 19 | float: right; 20 | margin-bottom: $base-line-height / 2; 21 | margin-top: 10px; 22 | padding: 10px 20px; 23 | width: 50%; 24 | border-radius: 2px; 25 | } 26 | 27 | .flash-alert { 28 | @extend %flash-base; 29 | @include flash($alert-color); 30 | } 31 | 32 | .flash-error { 33 | @extend %flash-base; 34 | @include flash($error-color); 35 | } 36 | 37 | .flash-notice { 38 | @extend %flash-base; 39 | @include flash($notice-color); 40 | } 41 | 42 | .flash-success { 43 | @extend %flash-base; 44 | @include flash($success-color); 45 | } 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/forms.scss: -------------------------------------------------------------------------------- 1 | [type=text], [type=password], [type=search], [type=email],[type=number], 2 | [type=url], [type=tel], textarea, select, input[type=radio], 3 | input[type=checkbox] { 4 | border:1px solid $base-border-color; 5 | border-radius:2px; 6 | background-color:#fff; 7 | color: $base-font-color; 8 | } 9 | 10 | input, textarea { 11 | font-family: $form-font-family; 12 | } 13 | 14 | [type=text],[type=password],[type=search],[type=email],[type=url],[type=tel],[type=number],textarea,select,input[type=radio],input[type=checkbox] { 15 | -webkit-transition:border-color .2s; 16 | transition:border-color .2s; 17 | } 18 | 19 | .invalid [type=text], .invalid [type=password], .invalid [type=email], .invalid [type=number], .invalid [type=url], .invalid [type=tel], .invalid textarea, .invalid select, .invalid [type=radio], .invalid [type=checkbox],[type=text]:invalid,[type=password]:invalid,[type=search]:invalid,[type=email]:invalid,[type=url]:invalid,[type=tel]:invalid,textarea:invalid,select:invalid,input[type=radio]:invalid,input[type=checkbox]:invalid { 20 | background-color: lighten($light-red, 3); 21 | border-color: darken($light-red, 20); 22 | } 23 | 24 | 25 | [type=text]:focus,[type=password]:focus,[type=search]:focus,[type=email]:focus,[type=url]:focus,[type=tel]:focus,[type=number]:focus,textarea:focus,select:focus,input[type=radio]:focus,input[type=checkbox]:focus { 26 | outline:none; 27 | border-color: darken($base-border-color, 20); 28 | } 29 | 30 | .disabled[type=text],.disabled[type=password],.disabled[type=search],.disabled[type=email],.disabled[type=url],.disabled[type=tel],textarea.disabled,select.disabled,input.disabled[type=radio],input.disabled[type=checkbox],[disabled][type=text],[disabled][type=password],[disabled][type=search],[disabled][type=email],[disabled][type=url],[disabled][type=tel],textarea[disabled],select[disabled],input[disabled][type=radio],input[disabled][type=checkbox],fieldset[disabled] [type=text],fieldset[disabled] [type=password],fieldset[disabled] [type=search],fieldset[disabled] [type=email],fieldset[disabled] [type=url],fieldset[disabled] [type=tel],fieldset[disabled] textarea,fieldset[disabled] select,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox] { 31 | color:#cacccd; 32 | cursor:default; 33 | } 34 | 35 | [type=text], [type=password], [type=search], [type=email], [type=url], [type=tel],[type=number], textarea, select { 36 | display:block; 37 | width:100%; 38 | padding:8px 10px; 39 | } 40 | 41 | [type=submit] { 42 | font-family: $base-font-family; 43 | } 44 | 45 | .invalid .error { 46 | color: darken($light-red, 40); 47 | } 48 | 49 | fieldset { 50 | margin: 0; 51 | border: 0; 52 | padding: 0; 53 | } 54 | 55 | label { 56 | display: block; 57 | padding-top: 9px; 58 | padding-bottom: 4px; 59 | font-weight: bold; 60 | } 61 | 62 | .hint { 63 | margin-top: -5px; 64 | padding-bottom: 8px; 65 | } 66 | 67 | select { 68 | display: block; 69 | width: 100%; 70 | height: 34px; 71 | padding: 6px 12px; 72 | font-size: 14px; 73 | line-height: 1.42857143; 74 | color: #555; 75 | background-color: #fff !important; 76 | background-image: none; 77 | border-radius: 2px; 78 | } 79 | 80 | select.date, select.datetime { 81 | width: auto; 82 | display: inline-block; 83 | } 84 | 85 | input[type=file] { 86 | display: block; 87 | } 88 | 89 | input[type=radio],input[type=checkbox] { 90 | position:relative; 91 | -webkit-appearance:none; 92 | height:1.25em; 93 | width:1.25em; 94 | margin-bottom:-.25em; 95 | margin-right:5px; 96 | vertical-align:top; 97 | } 98 | 99 | input[type=radio] { 100 | border-radius:1.25em; 101 | } 102 | 103 | input[type=radio]:checked:before { 104 | content:""; 105 | position:absolute; 106 | height:.45em; 107 | width:.45em; 108 | border-radius:.45em; 109 | top:50%; 110 | left:50%; 111 | margin-top:-.225em; 112 | margin-left:-.225em; 113 | background-color:#ff5a5f; 114 | } 115 | 116 | input[type=checkbox]:checked:before { 117 | content:"\f00c"; 118 | font-family: FontAwesome; 119 | position:absolute; 120 | font-size:.85em; 121 | left:1px; 122 | top: 2px; 123 | text-align:center; 124 | width:1.25em; 125 | color: lighten($base-font-color, 20); 126 | } 127 | 128 | form .cancel { 129 | padding-left: 20px; 130 | } 131 | 132 | form .form-error-message { 133 | color: $red; 134 | } 135 | 136 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/helpers.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin clear-user-select { 3 | -webkit-user-select: none; 4 | -moz-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | .text-center { text-align: center } 10 | .text-right { text-align: right } 11 | 12 | .text-muted { color: #82888a } 13 | 14 | .pull-left { float: left !important } 15 | .pull-right { float: right !important } 16 | 17 | .img-circle { border-radius: 50% } 18 | 19 | .fixed { 20 | position: fixed; 21 | } 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. 29 | * Correct `block` display not defined for `main` in IE 11. 30 | */ 31 | 32 | article, 33 | aside, 34 | details, 35 | figcaption, 36 | figure, 37 | footer, 38 | header, 39 | hgroup, 40 | main, 41 | nav, 42 | section, 43 | summary { 44 | display: block; 45 | } 46 | 47 | /** 48 | * 1. Correct `inline-block` display not defined in IE 8/9. 49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; /* 1 */ 57 | vertical-align: baseline; /* 2 */ 58 | } 59 | 60 | /** 61 | * Prevent modern browsers from displaying `audio` without controls. 62 | * Remove excess height in iOS 5 devices. 63 | */ 64 | 65 | audio:not([controls]) { 66 | display: none; 67 | height: 0; 68 | } 69 | 70 | /** 71 | * Address `[hidden]` styling not present in IE 8/9/10. 72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 73 | */ 74 | 75 | [hidden], 76 | template { 77 | display: none; 78 | } 79 | 80 | /* Links 81 | ========================================================================== */ 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | */ 86 | 87 | a { 88 | background: transparent; 89 | } 90 | 91 | /** 92 | * Improve readability when focused and also mouse hovered in all browsers. 93 | */ 94 | 95 | a:active, 96 | a:hover { 97 | outline: 0; 98 | } 99 | 100 | /* Text-level semantics 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 105 | */ 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | /** 112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bold; 118 | } 119 | 120 | /** 121 | * Address styling not present in Safari and Chrome. 122 | */ 123 | 124 | dfn { 125 | font-style: italic; 126 | } 127 | 128 | /** 129 | * Address variable `h1` font-size and margin within `section` and `article` 130 | * contexts in Firefox 4+, Safari, and Chrome. 131 | */ 132 | 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | 138 | /** 139 | * Address styling not present in IE 8/9. 140 | */ 141 | 142 | mark { 143 | background: #ff0; 144 | color: #000; 145 | } 146 | 147 | /** 148 | * Address inconsistent and variable font size in all browsers. 149 | */ 150 | 151 | small { 152 | font-size: 80%; 153 | } 154 | 155 | /** 156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 157 | */ 158 | 159 | sub, 160 | sup { 161 | font-size: 75%; 162 | line-height: 0; 163 | position: relative; 164 | vertical-align: baseline; 165 | } 166 | 167 | sup { 168 | top: -0.5em; 169 | } 170 | 171 | sub { 172 | bottom: -0.25em; 173 | } 174 | 175 | /* Embedded content 176 | ========================================================================== */ 177 | 178 | /** 179 | * Remove border when inside `a` element in IE 8/9/10. 180 | */ 181 | 182 | img { 183 | border: 0; 184 | } 185 | 186 | /** 187 | * Correct overflow not hidden in IE 9/10/11. 188 | */ 189 | 190 | svg:not(:root) { 191 | overflow: hidden; 192 | } 193 | 194 | /* Grouping content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Address margin not present in IE 8/9 and Safari. 199 | */ 200 | 201 | figure { 202 | margin: 1em 40px; 203 | } 204 | 205 | /** 206 | * Address differences between Firefox and other browsers. 207 | */ 208 | 209 | hr { 210 | -moz-box-sizing: content-box; 211 | box-sizing: content-box; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Contain overflow in all browsers. 217 | */ 218 | 219 | pre { 220 | overflow: auto; 221 | } 222 | 223 | /** 224 | * Address odd `em`-unit font size rendering in all browsers. 225 | */ 226 | 227 | code, 228 | kbd, 229 | pre, 230 | samp { 231 | font-family: monospace, monospace; 232 | font-size: 1em; 233 | } 234 | 235 | /* Forms 236 | ========================================================================== */ 237 | 238 | /** 239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 240 | * styling of `select`, unless a `border` property is set. 241 | */ 242 | 243 | /** 244 | * 1. Correct color not being inherited. 245 | * Known issue: affects color of disabled elements. 246 | * 2. Correct font properties not being inherited. 247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 248 | */ 249 | 250 | button, 251 | input, 252 | optgroup, 253 | select, 254 | textarea { 255 | color: inherit; /* 1 */ 256 | font: inherit; /* 2 */ 257 | margin: 0; /* 3 */ 258 | } 259 | 260 | /** 261 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 262 | */ 263 | 264 | button { 265 | overflow: visible; 266 | } 267 | 268 | /** 269 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 270 | * All other form control elements do not inherit `text-transform` values. 271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 272 | * Correct `select` style inheritance in Firefox. 273 | */ 274 | 275 | button, 276 | select { 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 282 | * and `video` controls. 283 | * 2. Correct inability to style clickable `input` types in iOS. 284 | * 3. Improve usability and consistency of cursor style between image-type 285 | * `input` and others. 286 | */ 287 | 288 | button, 289 | html input[type="button"], /* 1 */ 290 | input[type="reset"], 291 | input[type="submit"] { 292 | -webkit-appearance: button; /* 2 */ 293 | cursor: pointer; /* 3 */ 294 | } 295 | 296 | /** 297 | * Re-set default cursor for disabled elements. 298 | */ 299 | 300 | button[disabled], 301 | html input[disabled] { 302 | cursor: default; 303 | } 304 | 305 | /** 306 | * Remove inner padding and border in Firefox 4+. 307 | */ 308 | 309 | button::-moz-focus-inner, 310 | input::-moz-focus-inner { 311 | border: 0; 312 | padding: 0; 313 | } 314 | 315 | /** 316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 317 | * the UA stylesheet. 318 | */ 319 | 320 | input { 321 | line-height: normal; 322 | } 323 | 324 | /** 325 | * It's recommended that you don't attempt to style these elements. 326 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 327 | * 328 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 329 | * 2. Remove excess padding in IE 8/9/10. 330 | */ 331 | 332 | input[type="checkbox"], 333 | input[type="radio"] { 334 | box-sizing: border-box; /* 1 */ 335 | padding: 0; /* 2 */ 336 | } 337 | 338 | /** 339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 340 | * `font-size` values of the `input`, it causes the cursor style of the 341 | * decrement button to change from `default` to `text`. 342 | */ 343 | 344 | input[type="number"]::-webkit-inner-spin-button, 345 | input[type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 352 | * (include `-moz` to future-proof). 353 | */ 354 | 355 | input[type="search"] { 356 | -webkit-appearance: textfield; /* 1 */ 357 | -moz-box-sizing: content-box; 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | box-sizing: content-box; 360 | } 361 | 362 | /** 363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 364 | * Safari (but not Chrome) clips the cancel button when the search input has 365 | * padding (and `textfield` appearance). 366 | */ 367 | 368 | input[type="search"]::-webkit-search-cancel-button, 369 | input[type="search"]::-webkit-search-decoration { 370 | -webkit-appearance: none; 371 | } 372 | 373 | /** 374 | * Define consistent border, margin, and padding. 375 | */ 376 | 377 | fieldset { 378 | border: 1px solid #c0c0c0; 379 | margin: 0 2px; 380 | padding: 0.35em 0.625em 0.75em; 381 | } 382 | 383 | /** 384 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 386 | */ 387 | 388 | legend { 389 | border: 0; /* 1 */ 390 | padding: 0; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove default vertical scrollbar in IE 8/9/10/11. 395 | */ 396 | 397 | textarea { 398 | overflow: auto; 399 | } 400 | 401 | /** 402 | * Don't inherit the `font-weight` (applied by a rule above). 403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 404 | */ 405 | 406 | optgroup { 407 | font-weight: bold; 408 | } 409 | 410 | /* Tables 411 | ========================================================================== */ 412 | 413 | /** 414 | * Remove most spacing between table cells. 415 | */ 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/tables.scss: -------------------------------------------------------------------------------- 1 | 2 | .table, .wiki table { 3 | border-collapse: separate; 4 | margin: ($base-line-height / 2) 0; 5 | table-layout: auto; 6 | width: 100%; 7 | 8 | th { 9 | border-bottom: 1px solid darken($base-border-color, 5%); 10 | font-weight: bold; 11 | padding: ($base-line-height / 2) 0; 12 | text-align: left; 13 | } 14 | 15 | td { 16 | border-bottom: 1px solid $base-border-color; 17 | } 18 | 19 | tr:last-child td { 20 | border-bottom: none; 21 | } 22 | 23 | tr, td, th { 24 | vertical-align: middle; 25 | padding: 10px; 26 | } 27 | 28 | & > tbody > tr:nth-child(even) > td, 29 | & > tbody > tr:nth-child(even) > th { 30 | background-color: #f5f5f5; 31 | } 32 | 33 | td.actions a, 34 | td.actions span { 35 | opacity: 0; 36 | } 37 | 38 | tr:hover td.actions a, 39 | tr:hover td.actions span { 40 | opacity: 1; 41 | } 42 | 43 | a.destroy { 44 | color: red; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/typography.scss: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | -webkit-font-smoothing: antialiased; 4 | background-color: $base-background-color; 5 | color: $base-font-color; 6 | font-family: $base-font-family; 7 | font-size: $base-font-size; 8 | line-height: $base-line-height; 9 | font-weight: 500; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | font-family: $base-font-family; 14 | line-height: $header-line-height; 15 | margin: 0; 16 | text-rendering: optimizeLegibility; // Fix the character spacing for headings 17 | } 18 | 19 | h1 { 20 | font-size: $h1-font-size; 21 | font-weight: 500; 22 | } 23 | 24 | h2 { 25 | font-size: $h2-font-size; 26 | } 27 | 28 | h3 { 29 | font-size: $h3-font-size; 30 | } 31 | 32 | h4 { 33 | font-size: $h4-font-size; 34 | } 35 | 36 | h5 { 37 | font-size: $h5-font-size; 38 | } 39 | 40 | h6 { 41 | font-size: $h6-font-size; 42 | } 43 | 44 | p { 45 | margin: 0 0 ($base-line-height * .5); 46 | } 47 | 48 | p:first-child { 49 | margin-top: 0; 50 | } 51 | 52 | a { 53 | color: $base-link-color; 54 | text-decoration: none; 55 | 56 | &:hover { 57 | color: $hover-link-color; 58 | } 59 | 60 | &:active, &:focus { 61 | color: $hover-link-color; 62 | outline: none; 63 | } 64 | } 65 | 66 | hr { 67 | border-bottom: 1px solid $base-border-color; 68 | border-left: none; 69 | border-right: none; 70 | border-top: none; 71 | margin: $base-line-height 0; 72 | } 73 | 74 | img { 75 | margin: 0; 76 | max-width: 100%; 77 | } 78 | 79 | blockquote { 80 | border-left: 2px solid $base-border-color; 81 | color: lighten($base-font-color, 15); 82 | margin: $base-line-height 0; 83 | padding-left: $base-line-height / 2; 84 | } 85 | 86 | cite { 87 | color: lighten($base-font-color, 25); 88 | font-style: italic; 89 | 90 | &:before { 91 | content: '\2014 \00A0'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/components/variables.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | $georgia: Georgia, Cambria, "Times New Roman", Times, serif; 4 | $helvetica: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; 5 | $monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; 6 | 7 | $sans-serif: $helvetica; 8 | $serif: $georgia; 9 | $base-font-family: $sans-serif; 10 | $header-font-family: $base-font-family; 11 | 12 | // Sizes 13 | $base-font-size: 1em; 14 | $base-line-height: $base-font-size * 1.5; 15 | $unitless-line-height: $base-line-height / ($base-line-height * 0 + 1); // Strip units from line-height: https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#Prefer_unitless_numbers_for_line-height_values 16 | $header-line-height: $base-font-size * 1.25; 17 | $base-border-radius: em(3); 18 | $base-vertical-spacing: 0.5em; 19 | 20 | $h1-font-size: $base-font-size * 2.25; 21 | $h2-font-size: $base-font-size * 2; 22 | $h3-font-size: $base-font-size * 1.75; 23 | $h4-font-size: $base-font-size * 1.5; 24 | $h5-font-size: $base-font-size * 1.25; 25 | $h6-font-size: $base-font-size; 26 | 27 | // Colors 28 | $blue: #477DCA; 29 | $dark-gray: #333; 30 | $medium-gray: #999; 31 | $light-gray: #DDD; 32 | $light-red: #FBE3E4; 33 | $light-yellow: #FFF6BF; 34 | $light-green: #E6EFC2; 35 | 36 | // Background Color 37 | $base-background-color: white; 38 | 39 | // Font Colors 40 | $base-font-color: $dark-gray; 41 | $base-accent-color: $blue; 42 | 43 | // Link Colors 44 | $base-link-color: $base-accent-color; 45 | $hover-link-color: darken($base-accent-color, 15); 46 | $base-button-color: $base-link-color; 47 | $hover-button-color: $hover-link-color; 48 | 49 | // Border color 50 | $base-border-color: $light-gray; 51 | 52 | // Flash Colors 53 | $alert-color: $light-yellow; 54 | $error-color: $light-red; 55 | $notice-color: lighten($base-accent-color, 40); 56 | $success-color: $light-green; 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/navigation.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | background-color: $nav-background-color; 3 | @include clearfix; 4 | font-size: 0.9em; 5 | height: $nav-height; 6 | box-shadow: 0px 0px 5px 0px rgba(56,70,78,0.33); 7 | } 8 | 9 | .nav ul.nav-list { 10 | display: flex; 11 | margin-left: -14px; 12 | margin-top: 0; 13 | margin-bottom: 0; 14 | padding: 0 2rem; 15 | 16 | list-style: none; 17 | } 18 | 19 | .nav ul.nav-list > li.right { 20 | margin-left: auto; 21 | } 22 | 23 | .nav ul.nav-list > li.dropdown > a.nav-item { 24 | &:after { 25 | content: '\f3d0'; 26 | font-family: "Ionicons"; 27 | padding-left: 0.5em; 28 | } 29 | } 30 | 31 | .nav-item { 32 | color: $nav-link-color; 33 | position: relative; 34 | display: block; 35 | line-height: calc(#{$nav-height} - 3px); 36 | padding: 0 20px; 37 | font-weight: 500; 38 | 39 | i { 40 | margin-right: 6px; 41 | font-size: 24px; 42 | line-height: $nav-height; 43 | float: left; 44 | margin-top: -2px; 45 | } 46 | } 47 | 48 | .nav-item:hover, .nav-item:focus, .nav-item[aria-selected="true"] { 49 | color: $nav-link-color-active; 50 | text-decoration: none; 51 | border-bottom: 3px $nav-link-color-active solid; 52 | } 53 | 54 | .nav-item[aria-selected="true"]:before { 55 | position: absolute; 56 | content: ''; 57 | top: 0; 58 | left: 14px; 59 | right: 14px; 60 | height: 2px; 61 | background: $nav-active-highlight-color; 62 | z-index: 0; 63 | } 64 | 65 | .sliding-panel-content { 66 | $sliding-panel-border-color: darken($nav-background-color, 5%); 67 | $sliding-panel-background: #fff; 68 | $sliding-panel-color: #fff; 69 | $sliding-panel-border: 1px solid $sliding-panel-border-color; 70 | $sliding-panel-background-hover: $blue; 71 | $sliding-panel-color-hover: #fff; 72 | $sliding-panel-background-focus: lighten($sliding-panel-background, 5%); 73 | 74 | @include position(absolute, 0px 0px 0px auto); 75 | @include size(320px 100%); 76 | @include transform(translateX(320px)); 77 | @include transition(all 0.25s linear); 78 | 79 | background: $sliding-panel-background; 80 | z-index: 999; 81 | overflow-y: auto; 82 | -webkit-overflow-scrolling: touch; 83 | padding: 1em; 84 | box-shadow: -1px 0px 5px 0px rgba(56,70,78,0.33); 85 | 86 | &.is-visible { 87 | @include transform(translateX(0)); 88 | } 89 | } 90 | 91 | .sliding-panel-fade-screen { 92 | @include position(absolute, 0px 0px 0px 0px); 93 | opacity: 0; 94 | visibility: hidden; 95 | z-index: 998; 96 | 97 | &.is-visible { 98 | opacity: 0.4; 99 | visibility: visible; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/photosets.scss: -------------------------------------------------------------------------------- 1 | // Responsive Photosets 2 | // 3 | // Based heavily on/stolen from: https://medium.com/coding-design/responsive-photosets-7742e6f93d9e 4 | 5 | .photoset { 6 | overflow: hidden; 7 | width: 100%; 8 | 9 | .photoset-row { 10 | margin-bottom: .5rem; 11 | overflow: hidden; 12 | width: 150%; 13 | 14 | &:last-child { margin: 0; } 15 | } 16 | 17 | .photoset-item { 18 | display: block; 19 | float: left; 20 | margin: 0 .25rem; 21 | 22 | &:first-child { margin-left: 0 !important; } 23 | &:last-child { margin-right: 0 !important; } 24 | 25 | a { 26 | background-position: center center; 27 | background-size: cover; 28 | border: 0; 29 | display: block; 30 | position: relative; 31 | width: 100%; 32 | height: 100%; 33 | } 34 | } 35 | 36 | figure { 37 | margin: 0; 38 | overflow: hidden; 39 | position: relative; 40 | display: block; 41 | width: auto; 42 | height: 100%; 43 | } 44 | 45 | figcaption { 46 | background-color: rgba(0, 0, 0, .49); 47 | box-sizing: border-box; 48 | font-size: .75rem; 49 | padding: .2rem 1rem; 50 | position: absolute; 51 | bottom: 0; 52 | left: 0; 53 | width: 100%; 54 | color: #fff; 55 | text-align: left; 56 | } 57 | 58 | img { 59 | display: block; 60 | max-width: 100%; 61 | } 62 | 63 | @media screen and (min-width: 480px) and (max-width: 768px) { 64 | figcaption { 65 | display: none; 66 | } 67 | } 68 | 69 | @media screen and (max-width: 480px) { 70 | .photoset-row { 71 | margin-bottom: 1px; 72 | } 73 | 74 | .photoset-item { 75 | margin-right: 1px; 76 | margin-left: 0px; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/assets/stylesheets/plotline/shared.scss: -------------------------------------------------------------------------------- 1 | /* =Global styles 2 | ======================================================================= */ 3 | 4 | ::-moz-selection { 5 | background-color: $blue; 6 | color: #fff; 7 | } 8 | 9 | ::selection { 10 | background-color: $blue; 11 | color: #fff; 12 | } 13 | 14 | body { 15 | line-height: $base-line-height; 16 | -webkit-font-smoothing: antialiased; 17 | font-weight: 400; 18 | } 19 | 20 | .wrapper.content { 21 | min-height: 500px; 22 | padding-bottom: 100px; 23 | } 24 | 25 | .wrapper-flash { 26 | position: fixed; 27 | top: 60px; 28 | right: 50px; 29 | width: 500px; 30 | } 31 | 32 | .main-content { 33 | padding: 2rem; 34 | } 35 | 36 | #head { 37 | margin-bottom: 2rem; 38 | @include clearfix; 39 | border-bottom: 1px $base-border-color solid; 40 | padding-bottom: 1em; 41 | 42 | h1 { 43 | float: left; 44 | } 45 | 46 | a.btn { 47 | float: right; 48 | } 49 | } 50 | 51 | body.plain { 52 | form { 53 | width: 30em; 54 | margin: 100px auto; 55 | } 56 | } -------------------------------------------------------------------------------- /app/controllers/plotline/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class ApplicationController < ActionController::Base 3 | before_action :authenticate_user 4 | 5 | protected 6 | 7 | def current_user 8 | unless defined?(@current_user) 9 | @current_user = Plotline::User.find_by_auth_token(cookies[:auth_token]) if cookies[:auth_token].present? 10 | end 11 | @current_user 12 | end 13 | helper_method :current_user 14 | 15 | def authenticate_user 16 | return if current_user 17 | 18 | redirect_to signin_url 19 | end 20 | 21 | def content_entries_path 22 | entries_path(content_class: content_class.tableize) 23 | end 24 | helper_method :content_entries_path 25 | 26 | def content_entry_path(entry) 27 | entry_path(id: entry.id, content_class: content_class.tableize) 28 | end 29 | helper_method :content_entry_path 30 | 31 | def new_content_entry_path 32 | new_entry_path(content_class: content_class.tableize) 33 | end 34 | helper_method :new_content_entry_path 35 | 36 | def edit_content_entry_path(entry) 37 | edit_entry_path(id: entry.id, content_class: content_class.tableize) 38 | end 39 | helper_method :edit_content_entry_path 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/plotline/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "plotline/application_controller" 2 | 3 | module Plotline 4 | class DashboardController < ApplicationController 5 | def index 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/plotline/entries_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "plotline/application_controller" 2 | 3 | module Plotline 4 | class EntriesController < ApplicationController 5 | before_action :set_entry, only: [:show, :edit, :update, :destroy, :preview] 6 | 7 | def index 8 | @entries = Entry.where(type: content_class) 9 | end 10 | 11 | def show 12 | end 13 | 14 | def destroy 15 | @entry.destroy 16 | redirect_to content_entries_path, notice: 'Entry was successfully destroyed.' 17 | end 18 | 19 | private 20 | 21 | def set_entry 22 | @entry = Entry.find(params[:id]) 23 | end 24 | 25 | def content_class 26 | @content_class ||= params[:content_class].classify 27 | end 28 | helper_method :content_class 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/plotline/images_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "plotline/application_controller" 2 | 3 | module Plotline 4 | class ImagesController < ApplicationController 5 | def index 6 | @images = Image.order('id desc') 7 | end 8 | 9 | def show 10 | @image = Image.find(params[:id]) 11 | end 12 | 13 | def destroy 14 | @image = Image.find(params[:id]) 15 | @image.destroy 16 | redirect_to :back, notice: 'Image was successfully destroyed.' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/plotline/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "plotline/application_controller" 2 | 3 | module Plotline 4 | class SessionsController < ApplicationController 5 | skip_before_action :authenticate_user 6 | 7 | layout 'plotline/plain' 8 | 9 | def new 10 | end 11 | 12 | def create 13 | user = User.find_by_email(params[:email]) 14 | if user && user.authenticate(params[:password]) 15 | cookies.permanent[:auth_token] = user.auth_token 16 | 17 | flash[:notice] = "Hello!" 18 | redirect_to root_url 19 | else 20 | flash.now.alert = "Invalid email and/or password" 21 | render "new" 22 | end 23 | end 24 | 25 | def destroy 26 | cookies.delete(:auth_token) 27 | redirect_to root_url 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/plotline/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module ApplicationHelper 3 | def title(title = nil) 4 | if title 5 | content_for(:title) { title } 6 | else 7 | content_for(:title) 8 | end 9 | end 10 | 11 | def body_class(options = {}) 12 | controller_name = controller.controller_path.gsub('/','-') 13 | basic_body_class = "#{controller_name} #{controller_name}-#{controller.action_name}" 14 | 15 | if content_for?(:body_class) 16 | [basic_body_class, content_for(:body_class)].join(' ') 17 | else 18 | basic_body_class 19 | end 20 | end 21 | 22 | def present(object, klass = nil) 23 | begin 24 | klass ||= "#{object.class}Presenter".constantize 25 | rescue NameError 26 | klass = "#{object.class.superclass}Presenter".constantize 27 | end 28 | presenter = klass.new(object, self) 29 | yield presenter if block_given? 30 | presenter 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/plotline/concerns/family.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Concerns 3 | module Family 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | belongs_to :parent, class_name: 'Plotline::Entry' 8 | has_many :children, class_name: 'Plotline::Entry', foreign_key: :parent_id 9 | end 10 | 11 | module ClassMethods 12 | def parent_entry(parent_entry_klass = nil) 13 | if parent_entry_klass 14 | @parent_entry ||= parent_entry_klass.to_s.classify 15 | alias_method parent_entry_klass, :parent 16 | end 17 | @parent_entry 18 | end 19 | 20 | def child_entries(child_entry_klass) 21 | @child_entry ||= child_entry_klass.to_s.singularize.classify 22 | alias_method child_entry_klass, :children 23 | end 24 | end 25 | 26 | def can_have_parent? 27 | self.class.parent_entry.present? 28 | end 29 | 30 | def possible_parents 31 | Plotline::Entry.where(type: self.class.parent_entry) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/plotline/concerns/searchable.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Concerns 3 | module Searchable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | has_many :search_data, class_name: 'Plotline::EntrySearchData' 8 | 9 | after_save :update_search_index 10 | end 11 | 12 | module ClassMethods 13 | def searchable_attributes(*args) 14 | @searchable_attributes = args if args.any? 15 | @searchable_attributes ||= [] 16 | end 17 | 18 | def search(query) 19 | select('DISTINCT ON (entry_id) entry_id, plotline_entries.*'). 20 | joins(:search_data). 21 | where("search_data @@ plainto_tsquery('english', :q)", q: query). 22 | order("entry_id, ts_rank(search_data, plainto_tsquery('%s')) desc" % send(:sanitize_sql, query)) 23 | end 24 | end 25 | 26 | def search_attributes 27 | self.class.searchable_attributes.each_with_object({}) do |attr_name, search_data| 28 | search_data[attr_name] = send(attr_name) 29 | end 30 | end 31 | 32 | def update_search_index 33 | Plotline::EntrySearchData.index_entry_data(id, search_attributes) 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /app/models/plotline/concerns/taggable.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Concerns 3 | module Taggable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | attr_accessor :raw_tags 8 | before_save :split_raw_tags 9 | end 10 | 11 | module ClassMethods 12 | def tagged_with(tag) 13 | where('tags @> ?', "{#{tag}}") 14 | end 15 | end 16 | 17 | private 18 | 19 | def split_raw_tags 20 | return if raw_tags.blank? 21 | 22 | self.tags = raw_tags.split(',').map { |t| t.strip } 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/models/plotline/entry.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class Entry < ActiveRecord::Base 3 | include Plotline::Concerns::Searchable 4 | include Plotline::Concerns::Taggable 5 | include Plotline::Concerns::Family 6 | 7 | enum status: [ :draft, :published ] 8 | 9 | belongs_to :user 10 | 11 | scope :drafts, -> { where(status: :draft) } 12 | scope :published, -> { where(status: :published).where('published_at <= ?', Time.zone.now) } 13 | 14 | validates :title, presence: true 15 | validates :slug, uniqueness: { scope: :type, allow_blank: true } 16 | 17 | def self.content_attr(attr_name, attr_type = :string) 18 | content_attributes[attr_name] = attr_type 19 | 20 | define_method(attr_name) do 21 | self.payload ||= {} 22 | self.payload[attr_name.to_s] 23 | end 24 | 25 | define_method("#{attr_name}=".to_sym) do |value| 26 | self.payload ||= {} 27 | self.payload[attr_name.to_s] = value 28 | end 29 | end 30 | 31 | def self.content_attributes 32 | @content_attributes ||= {} 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/plotline/entry_search_data.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class EntrySearchData < ActiveRecord::Base 3 | belongs_to :entry 4 | 5 | validates :entry_id, presence: true 6 | validates :attr_name, presence: true, uniqueness: { scope: :entry_id } 7 | 8 | def self.index_entry_data(entry_id, search_attributes) 9 | search_attributes.each do |attr_name, value| 10 | find_or_create_by(entry_id: entry_id, attr_name: attr_name) 11 | 12 | where(entry_id: entry_id, attr_name: attr_name).update_all([ 13 | "raw_data = :search_data, search_data = to_tsvector('english', :search_data)", 14 | search_data: value 15 | ]) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/plotline/image.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class Image < ActiveRecord::Base 3 | before_save :set_metadata 4 | after_destroy :remove_image_file 5 | 6 | def filename 7 | remote_file? ? image : File.join('./public', image) 8 | end 9 | 10 | def remove_image_file 11 | File.delete(filename) unless remote_file? 12 | end 13 | 14 | private 15 | 16 | def set_metadata 17 | img = FastImage.new(filename) 18 | 19 | self.width, self.height = img.size 20 | self.ratio = self.width.to_f / self.height.to_f 21 | self.content_type = img.type 22 | self.file_size = img.content_length 23 | 24 | return if remote_file? 25 | 26 | File.open(filename) do |file| 27 | self.file_size = file.size # content_length doesn't always work 28 | end 29 | 30 | self.exif = Exiftool.new(filename).to_hash 31 | end 32 | 33 | def remote_file? 34 | image.start_with?('http') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/plotline/user.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class User < ActiveRecord::Base 3 | has_secure_password 4 | 5 | validates :email, presence: true, uniqueness: true 6 | validates :password, presence: { unless: :persisted? }, length: { minimum: 6 }, allow_nil: true 7 | 8 | before_create { generate_token(:auth_token) } 9 | 10 | private 11 | 12 | def generate_token(column) 13 | begin 14 | self[column] = SecureRandom.urlsafe_base64 15 | end while Plotline::User.exists?(column => self[column]) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/presenters/plotline/base_presenter.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class BasePresenter 3 | def initialize(object, template) 4 | @object = object 5 | @template = template 6 | end 7 | 8 | private 9 | 10 | def self.presents(name) 11 | define_method(name) do 12 | @object 13 | end 14 | end 15 | 16 | def h 17 | @template 18 | end 19 | 20 | def method_missing(*args, &block) 21 | # this is probably too hacky 22 | if @object.respond_to?(args.first) 23 | @object.send(*args, &block) 24 | else 25 | @template.send(*args, &block) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/presenters/plotline/entry_presenter.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class EntryPresenter < BasePresenter 3 | presents :entry 4 | 5 | def body_markdown 6 | text = parse_custom_markdown(@object.body.to_s) 7 | RDiscount.new(text, :smart, :footnotes).to_html.html_safe 8 | end 9 | 10 | def photoset_item(src:, alt:, attrs:) 11 | img = images_hash[src] 12 | attrs["class"] = "photoset-item " + attrs["class"].to_s 13 | 14 | photoset_item_html(img, src, alt, attrs, image_attrs(img)) 15 | end 16 | 17 | def photoset_item_html(img, src, alt, attrs, image_attrs) 18 | content_tag(:figure, attrs) do 19 | concat image_tag(src, { alt: alt }.merge(image_attrs)) 20 | concat content_tag(:figcaption, alt) 21 | end 22 | end 23 | 24 | def single_image_html(src:, alt:, attrs:) 25 | content_tag(:figure, attrs) do 26 | concat image_tag(src, alt: alt) 27 | concat content_tag(:figcaption, alt) 28 | end 29 | end 30 | 31 | def image_attrs(img) 32 | return { data: {} } unless img 33 | 34 | { data: { width: img.width, height: img.height, ratio: img.ratio } } 35 | end 36 | 37 | private 38 | 39 | def parse_custom_markdown(text) 40 | Plotline::CustomMarkdownParser.new(self).parse(text) 41 | end 42 | 43 | def images_hash 44 | @images_hash ||= Plotline::Image.all.each_with_object({}) do |img, hash| 45 | hash[img.image] = img 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/views/layouts/plotline/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> | Plotline 5 | 6 | <%= javascript_tag do %> 7 | var Plotline = {}; 8 | <% end %> 9 | 10 | 11 | 12 | 13 | <%= stylesheet_link_tag "plotline/application", media: "all" %> 14 | <%= javascript_include_tag "plotline/application" %> 15 | <%= csrf_meta_tags %> 16 | 17 | 18 | <%= render partial: "plotline/shared/navbar" %> 19 | 20 |

21 | <%= render partial: "plotline/shared/flash_messages", flash: flash %> 22 |
23 | 24 |
25 | <% if content_for?(:heading) %> 26 | 29 | <% end %> 30 | 31 | <%= yield %> 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /app/views/layouts/plotline/plain.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Plotline 5 | 6 | 7 | 8 | <%= stylesheet_link_tag "plotline/application", media: "all" %> 9 | <%= csrf_meta_tags %> 10 | 11 | 12 |
13 | <%= yield %> 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /app/views/plotline/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Dashboard" %> 2 | 3 |

Dashboard

4 |

Hello

5 | -------------------------------------------------------------------------------- /app/views/plotline/entries/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title content_class.tableize.titleize %> 2 | 3 | <% content_for :heading do %> 4 |

<%= content_class.tableize.titleize %>

5 | <% end %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @entries.each do |entry| %> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% end %> 28 | 29 |
TitleCreated atUpdated atPublished at
<%= link_to entry.title, content_entry_path(entry) %><%= time_ago_in_words entry.created_at %> ago<%= time_ago_in_words entry.updated_at %> ago<%= entry.published_at %><%= link_to 'Destroy', content_entry_path(entry), method: :delete, data: { confirm: 'Are you sure?' } %>
30 | -------------------------------------------------------------------------------- /app/views/plotline/entries/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @entry.title %> 2 | 3 | <% content_for :heading do %> 4 |

<%= @entry.title %>

5 | <% end %> 6 | 7 | <% present(@entry, Plotline::EntryPresenter) do |entry| %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @entry.class.content_attributes.each do |attr_name, attr_type| %> 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Slug:<%= @entry.slug %>
Tags:<%= @entry.tags.join(', ') %>
<%= attr_name.to_s.titleize %>:<%= @entry.send(attr_name.to_sym) %>
27 | 28 |
29 | <%= entry.body_markdown %> 30 |
31 | <% end %> 32 | -------------------------------------------------------------------------------- /app/views/plotline/images/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Images" %> 2 | 3 | <% content_for :heading do %> 4 |

Images

5 | <% end %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% @images.each do |image| %> 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | <% end %> 32 | 33 |
ImageNameSizeCreated at
22 | <%= link_to plotline.image_path(image) do %> 23 | <%= image_tag image.image, width: 100 %> 24 | <% end %> 25 | <%= image.image %><%= number_to_human_size image.file_size %><%= time_ago_in_words image.created_at %> ago<%= link_to 'Destroy', plotline.image_path(image), method: :delete, data: { confirm: 'Are you sure?' } %>
34 | -------------------------------------------------------------------------------- /app/views/plotline/images/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Image: #{@image.image}" %> 2 | 3 | <% content_for :heading do %> 4 |

Image

5 | <% end %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @image.exif.each do |key, value| %> 16 | 17 | 18 | 19 | 20 | <% end %> 21 | 22 |
Filename:<%= @image.image %><%= image_tag @image.image, width: 300 %>
<%= key.to_s.titleize %>:<%= value %>
23 | -------------------------------------------------------------------------------- /app/views/plotline/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag(sessions_path, class: 'no-labels') do %> 2 |
3 |
4 | <% if flash[:alert].present? %> 5 |

<%= flash[:alert] %>

6 | <% end %> 7 | 8 |
9 |
<%= text_field_tag :email, params[:email], placeholder: 'E-mail' %>
10 |
<%= password_field_tag :password, nil, placeholder: 'Password' %>
11 |
12 | 13 | <%= submit_tag 'Sign in', class: 'btn btn-primary full-width' %> 14 |
15 |
16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/plotline/shared/_flash_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |type, message| %> 2 |
3 | × 4 | <%= message %> 5 |
6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/plotline/shared/_navbar.html.erb: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /bin/plotline: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'commander/import' 3 | require 'listen' 4 | require 'plotline/version' 5 | require File.join(Dir.pwd, 'config', 'environment') # load the Rails app 6 | 7 | program :name, 'Plotline' 8 | program :version, Plotline::VERSION 9 | program :description, 'Simple CMS based on Ruby, Markdown and Postgres' 10 | 11 | command :sync do |c| 12 | c.syntax = 'plotline sync [options]' 13 | c.description = 'Sync content files from --path to the database and --media-target directories' 14 | c.option '--source-path STRING', String, 'Directory with content files' 15 | c.option '--target STRING', String, 'Rails application directory (defaults to pwd)' 16 | c.action do |args, options| 17 | options.default target: Dir.pwd 18 | 19 | importer = Plotline::Import::Runner.new(options.source_path, options.target) 20 | importer.import_all! 21 | 22 | listener = Listen.to(options.source_path) do |modified, added, removed| 23 | importer.process_files(removed | modified | added) 24 | end 25 | 26 | listener.start 27 | sleep 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/plotline/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Plotline::Engine.routes.draw do 2 | resources :images, only: [:index, :show, :destroy] 3 | 4 | scope "/:content_class" do 5 | resources :entries, except: [:new, :create, :edit, :update] 6 | end 7 | 8 | get 'sign-in', to: 'sessions#new', as: 'signin' 9 | get 'sign-out', to: 'sessions#destroy', as: 'signout' 10 | 11 | resources :sessions, only: [:new, :create, :destroy] 12 | 13 | root to: 'dashboard#index' 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20150831131759_create_plotline_entries.plotline.rb: -------------------------------------------------------------------------------- 1 | class CreatePlotlineEntries < ActiveRecord::Migration 2 | def change 3 | create_table :plotline_entries do |t| 4 | t.string :type, index: true 5 | t.string :title 6 | t.string :slug, index: true 7 | t.text :body 8 | t.json :payload 9 | t.datetime :published_at 10 | t.integer :status, default: 0, index: true 11 | t.text :tags, array: true, default: [] 12 | t.integer :parent_id, index: true 13 | t.string :checksum 14 | 15 | t.timestamps null: false 16 | end 17 | 18 | add_index :plotline_entries, :tags, using: 'gin' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20150911135536_create_plotline_entry_search_data.plotline.rb: -------------------------------------------------------------------------------- 1 | class CreatePlotlineEntrySearchData < ActiveRecord::Migration 2 | def change 3 | create_table :plotline_entry_search_data do |t| 4 | t.integer :entry_id, index: true 5 | t.string :attr_name 6 | t.tsvector :search_data 7 | t.text :raw_data 8 | 9 | t.timestamps null: false 10 | end 11 | 12 | execute 'create index idx_search_data on plotline_entry_search_data using gin(search_data)' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20160212172219_create_plotline_images.plotline.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from plotline (originally 20160208102834) 2 | class CreatePlotlineImages < ActiveRecord::Migration 3 | def change 4 | create_table :plotline_images do |t| 5 | t.string :image 6 | t.integer :width 7 | t.integer :height 8 | t.float :ratio 9 | t.integer :file_size 10 | t.string :content_type 11 | t.json :exif 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20160224161843_create_plotline_users.plotline.rb: -------------------------------------------------------------------------------- 1 | class CreatePlotlineUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :plotline_users do |t| 4 | t.string :email 5 | t.string :name 6 | t.string :password_digest 7 | t.string :auth_token 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/plotline.rb: -------------------------------------------------------------------------------- 1 | require "plotline/engine" 2 | require "plotline/configuration" 3 | require "plotline/custom_markdown_parser" 4 | require "plotline/import/handlers/base" 5 | require "plotline/import/handlers/image_file" 6 | require "plotline/import/handlers/video_file" 7 | require "plotline/import/handlers/markdown_file" 8 | require "plotline/import/runner" 9 | 10 | module Plotline 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/plotline/configuration.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class Configuration 3 | attr_accessor :content_classes 4 | 5 | def initialize 6 | @content_classes = [].freeze 7 | end 8 | 9 | def logger(logger = nil) 10 | @logger ||= logger || Logger.new(STDOUT) 11 | @logger 12 | end 13 | end 14 | 15 | def self.configuration 16 | @configuration ||= Configuration.new 17 | end 18 | 19 | def self.configure 20 | yield configuration 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/plotline/custom_markdown_parser.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class CustomMarkdownParser 3 | def initialize(presenter) 4 | @presenter = presenter 5 | end 6 | 7 | # Matches photoset enclosing tags: 8 | # ... 9 | PHOTOSET_PATTERN = /---(\s*\n.*?)\n?^---\s*$\n?/m 10 | 11 | # Matches a single image in the Markdown format: 12 | # ![alt text](/path/to/image.jpg) 13 | IMAGE_PATTERN = /\!\[([^\]]*)\]\(([^)]+)\)(\{([^{]+)\})?/ 14 | 15 | PHOTOSET_HTML = "
%{rows}
\n\n" 16 | PHOTOSET_ROW_HTML = "
%{items}
" 17 | 18 | def parse(text) 19 | text = parse_photosets(text) 20 | text = parse_single_images(text) 21 | 22 | text 23 | end 24 | 25 | def parse_photosets(text) 26 | text.gsub(PHOTOSET_PATTERN) do |s| 27 | # Photoset row is a a set of images separated by 2 new line characters 28 | rows = $1.gsub("\r", "").strip.split("\n\n").map do |row| 29 | # Each line in row is considered an "item" (image) 30 | items = row.split("\n").reject { |i| i.strip.blank? } 31 | images = items.map { |image| parse_image(image, :photoset_item) } 32 | 33 | PHOTOSET_ROW_HTML % { items: images.join("\n") } 34 | end 35 | 36 | PHOTOSET_HTML % { rows: "\n" + rows.join("\n") + "\n" } 37 | end 38 | end 39 | 40 | def parse_single_images(text) 41 | parse_image(text, :single_image_html) 42 | end 43 | 44 | private 45 | 46 | def parse_image(text, callback) 47 | text.gsub(IMAGE_PATTERN) do 48 | attrs = parse_special_attributes($4) 49 | 50 | item = @presenter.send(callback, src: $2, alt: $1, attrs: attrs) 51 | item = item.gsub(/
\s?<\/figcaption>/, '') # remove empty captions: 52 | item.gsub(/^\s+/, '') # remove indentation from the beginning of lines 53 | end 54 | end 55 | 56 | # Parses additional attributes placed within brackets: 57 | # 58 | # ![](/foo.jpg){.regular #hero lang=fr} 59 | # ![](/bar.jpg){.big #the-site data-behavior=lightbox} 60 | # 61 | # Note: works with images only. 62 | def parse_special_attributes(raw_attrs) 63 | return {} if raw_attrs.blank? 64 | items = raw_attrs.split(/\s+/) 65 | 66 | id = items.select { |i| i =~ /^#.+/ }.first.gsub('#', '') 67 | classes = items.select { |i| i =~ /^\..+/ }.map { |c| c.gsub('.', '') } 68 | attrs = Hash[items.select { |i| i.include?('=') }.map { |i| i.split('=') }] 69 | 70 | attrs.merge({ 71 | 'id' => id, 72 | 'class' => classes.join(' ') 73 | }) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/plotline/engine.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Plotline 4 | 5 | require 'jquery-rails' 6 | require 'rdiscount' 7 | require 'fastimage' 8 | require 'exiftool' 9 | 10 | require 'bourbon' 11 | require 'autoprefixer-rails' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/plotline/import/handlers/base.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Import 3 | module Handlers 4 | class Base 5 | def initialize(runner) 6 | @runner = runner 7 | end 8 | 9 | def supported_file?(filename) 10 | raise NotImplementedError 11 | end 12 | 13 | private 14 | 15 | def log(msg) 16 | Plotline.configuration.logger.info(msg) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/plotline/import/handlers/image_file.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Import 3 | module Handlers 4 | class ImageFile < Base 5 | IMAGE_EXTENSIONS = %w(jpg jpeg png gif bmp tiff).freeze 6 | 7 | def supported_file?(filename) 8 | IMAGE_EXTENSIONS.include?(File.extname(filename).gsub('.', '')) 9 | end 10 | 11 | def import(filename) 12 | log "\e[34mImporting:\e[0m #{filename}" 13 | 14 | if !File.exists?(filename) 15 | log "FILE REMOVED" 16 | return 17 | end 18 | 19 | dst = filename.gsub(@runner.source_dir, @runner.uploads_dir) 20 | 21 | FileUtils.mkdir_p(File.dirname(dst)) 22 | FileUtils.cp(filename, dst) 23 | 24 | file = dst.gsub(@runner.public_dir, '') 25 | image = Plotline::Image.find_or_initialize_by(image: file) 26 | return if image.persisted? && File.size(dst) == image.file_size 27 | 28 | image.save! 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/plotline/import/handlers/markdown_file.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Import 3 | module Handlers 4 | class MarkdownFile < Base 5 | FILENAME_SPLIT_PATTERN = /^(\d{4}-\d{2}-\d{2})-(.*)/ 6 | FRONT_MATTER_PATTERN = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m 7 | MARKDOWN_EXTENSIONS = %w(md markdown).freeze 8 | 9 | def supported_file?(filename) 10 | MARKDOWN_EXTENSIONS.include?(File.extname(filename).gsub('.', '')) 11 | end 12 | 13 | def import(filename) 14 | log "\e[34mImporting:\e[0m #{filename}" 15 | 16 | date, slug = filename_to_date_and_slug(filename) 17 | 18 | if !File.exists?(filename) && entry = Plotline::Entry.find_by(slug: slug) 19 | log " \e[31mFile removed, deleting entry\e[0m \e[32m##{entry.id}\e[0m" 20 | entry.destroy 21 | return 22 | end 23 | 24 | full_contents = File.read(filename) 25 | full_contents = convert_relative_image_paths(filename, full_contents) 26 | 27 | meta, contents = extract_metadata_from_contents(full_contents) 28 | 29 | if meta['type'].blank? 30 | raise "\e[31mMissing 'type' attribute in #{filename}\e[0m" 31 | end 32 | 33 | klass = meta.delete('type').classify.constantize 34 | entry = klass.find_or_initialize_by(slug: slug) 35 | 36 | process_image_urls(full_contents) 37 | update_entry(entry, meta, date, contents, full_contents) 38 | end 39 | 40 | private 41 | 42 | # Turns markdown filename to date and slug, e.g.: 43 | # 2016-03-20_hello-world.md 44 | # results in: 45 | # ['2016-03-20', 'hello-world'] 46 | # 47 | # If there's no date in the filename (e.g. when file is a draft), 48 | # only slug will be returned and date will be nil. 49 | def filename_to_date_and_slug(filename) 50 | date, slug = File.basename(filename, ".*").split(FILENAME_SPLIT_PATTERN).reject { |m| m.blank? } 51 | if slug.blank? 52 | slug = date 53 | date = nil 54 | end 55 | 56 | [date, slug] 57 | end 58 | 59 | def extract_metadata_from_contents(contents) 60 | if result = contents.match(FRONT_MATTER_PATTERN) 61 | contents = contents[(result[0].length)...(contents.length)] 62 | meta = YAML.safe_load(result[0], [Date]) 63 | else 64 | meta = {} 65 | end 66 | 67 | [meta, contents] 68 | end 69 | 70 | # Converts relative image paths found in markdown files 71 | # to the target path in app/public 72 | def convert_relative_image_paths(filename, contents) 73 | entry_file_dir = File.dirname(filename) 74 | 75 | contents.gsub(/(\.\.?\/.+\.(?:jpe?g|gif|png|mp4|mov|wmv|avi))/) do 76 | absolute_path = File.expand_path(File.join(entry_file_dir, $1)) 77 | '/uploads' + absolute_path.gsub(@runner.source_dir, '') 78 | end 79 | end 80 | 81 | def process_image_urls(contents) 82 | URI.extract(contents).select{ |url| url[/\.(?:jpe?g|png|gif)\b/i] }.each do |url| 83 | Plotline::Image.find_or_create_by(image: url) 84 | end 85 | end 86 | 87 | def update_entry(entry, meta, date, contents, full_contents) 88 | checksum = Digest::MD5.hexdigest(full_contents) 89 | if entry.checksum == checksum 90 | log " File unchanged, skipping." 91 | return 92 | end 93 | 94 | draft = !!meta.delete('draft') 95 | entry.status = draft ? :draft : :published 96 | 97 | entry.assign_attributes(meta.merge( 98 | body: contents, 99 | checksum: checksum, 100 | published_at: (Date.parse(date) if date && !draft) 101 | )) 102 | 103 | dump_log(entry, meta) 104 | 105 | unless entry.save 106 | dump_errors(entry) 107 | end 108 | rescue ActiveModel::UnknownAttributeError => e 109 | log "\e[31mERROR: #{e.message}\e[0m" 110 | end 111 | 112 | def dump_log(entry, meta) 113 | log "\e[32m#{entry.class.name}:\e[0m" 114 | meta.each do |k, v| 115 | log " \e[32m#{k}:\e[0m #{v}" 116 | end 117 | log " \e[32mslug:\e[0m #{entry.slug}" 118 | log " \e[32mdraft:\e[0m #{entry.draft?}" 119 | log " \e[32mpublished_at:\e[0m #{entry.published_at}" 120 | log " \e[32mbody:\e[0m #{entry.body[0..100].gsub("\n", " ")}..." 121 | end 122 | 123 | def dump_errors(entry) 124 | log "\e[31mERROR: #{entry.class.name} could not be saved!\e[0m" 125 | entry.errors.each do |attr, error| 126 | log " \e[31m#{attr}:\e[0m #{error}" 127 | end 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/plotline/import/handlers/video_file.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Import 3 | module Handlers 4 | class VideoFile < Base 5 | IMAGE_EXTENSIONS = %w(mov mp4 avi wmv).freeze 6 | 7 | def supported_file?(filename) 8 | IMAGE_EXTENSIONS.include?(File.extname(filename).gsub('.', '')) 9 | end 10 | 11 | def import(filename) 12 | log "\e[34mImporting:\e[0m #{filename}" 13 | 14 | if !File.exists?(filename) 15 | log "FILE REMOVED" 16 | return 17 | end 18 | 19 | dst = filename.gsub(@runner.source_dir, @runner.uploads_dir) 20 | 21 | FileUtils.mkdir_p(File.dirname(dst)) 22 | FileUtils.cp(filename, dst) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/plotline/import/runner.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | module Import 3 | class UnsupportedFileType < StandardError; end 4 | 5 | class Runner 6 | HANDLERS = [ 7 | Plotline::Import::Handlers::MarkdownFile, 8 | Plotline::Import::Handlers::ImageFile, 9 | Plotline::Import::Handlers::VideoFile 10 | ].freeze 11 | 12 | # So far this includes only the annoying Icon\r file on OSX, which 13 | # is hidden, but it's not a dotfile, so Dir lookup doesn't ignore it... 14 | # 15 | # This file appears when a directory has a custom icon (e.g shared 16 | # dropbox folder). 17 | IGNORED_FILES = [ 18 | "Icon\r" 19 | ].freeze 20 | 21 | attr_reader :source_dir, :target_dir, :public_dir, :uploads_dir 22 | 23 | def initialize(source_dir, target_dir) 24 | @source_dir = source_dir 25 | @target_dir = target_dir 26 | @public_dir = target_dir + '/public' 27 | @uploads_dir = target_dir + '/public/uploads' 28 | 29 | @handlers = HANDLERS.map { |klass| klass.new(self) } 30 | end 31 | 32 | def import_all! 33 | process_files(Dir[@source_dir + '/**/*']) 34 | end 35 | 36 | def process_files(files) 37 | files.each do |filename| 38 | next if FileTest.directory?(filename) 39 | next if IGNORED_FILES.include?(File.basename(filename)) 40 | 41 | handler_found = false 42 | @handlers.each do |handler| 43 | if handler.supported_file?(filename) 44 | handler.import(filename) 45 | handler_found = true 46 | end 47 | end 48 | 49 | raise UnsupportedFileType.new(filename) unless handler_found 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/plotline/version.rb: -------------------------------------------------------------------------------- 1 | module Plotline 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/plotline_tasks.rake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/lib/tasks/plotline_tasks.rake -------------------------------------------------------------------------------- /plotline.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "plotline/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "plotline" 9 | s.version = Plotline::VERSION 10 | s.authors = ["Piotr Chmolowski"] 11 | s.email = ["piotr@chmolowski.pl"] 12 | s.homepage = "https://github.com/pch/plotline" 13 | s.summary = "Markdown & Postres-based CMS engine for Rails." 14 | s.description = "Markdown & Postres-based CMS engine for Rails." 15 | s.license = "MIT" 16 | 17 | s.required_ruby_version = ">= 2.3.0" 18 | 19 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 20 | s.test_files = Dir["test/**/*"] 21 | 22 | s.executables = ["plotline"] 23 | 24 | s.add_dependency "rails", "~> 5.0.0.1" 25 | s.add_dependency "bcrypt", "~> 3.1.7" 26 | s.add_dependency "sass-rails", "~> 5.0" 27 | 28 | s.add_dependency "jquery-rails" 29 | s.add_dependency "autoprefixer-rails" 30 | s.add_dependency "bourbon" 31 | 32 | # Files & images 33 | s.add_dependency "fastimage" 34 | s.add_dependency "exiftool" 35 | 36 | # Markdown 37 | s.add_dependency "rdiscount" 38 | 39 | # File sync 40 | s.add_dependency "commander" 41 | s.add_dependency "listen" 42 | 43 | s.add_development_dependency "pg" 44 | end 45 | -------------------------------------------------------------------------------- /test/controllers/plotline/dashboard_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class DashboardControllerTest < ActionController::TestCase 5 | setup do 6 | @routes = Engine.routes 7 | Plotline::DashboardController.any_instance.stubs(:current_user).returns(User.new) 8 | end 9 | 10 | test "should get index" do 11 | get :index 12 | assert_response :success 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/controllers/plotline/entries_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class EntriesControllerTest < ActionController::TestCase 5 | setup do 6 | @entry = plotline_entries(:sample) 7 | @routes = Engine.routes 8 | 9 | EntriesController.any_instance.stubs(:current_user).returns(Plotline::User.new) 10 | end 11 | 12 | test "should redirect to sign_in_url if not logged in" do 13 | EntriesController.any_instance.stubs(:current_user).returns(nil) 14 | 15 | get :index, params: { content_class: 'BlogPost' } 16 | assert_redirected_to '/plotline/sign-in' 17 | end 18 | 19 | test "should get index" do 20 | get :index, params: { content_class: 'BlogPost' } 21 | assert_response :success 22 | assert_not_nil assigns(:entries) 23 | end 24 | 25 | test "should show entry" do 26 | get :show, params: { id: @entry, content_class: 'BlogPost' } 27 | assert_response :success 28 | end 29 | 30 | test "should destroy entry" do 31 | assert_difference('Entry.count', -1) do 32 | delete :destroy, params: { content_class: 'BlogPost', id: @entry } 33 | end 34 | 35 | assert_redirected_to entries_path(content_class: 'blog_posts') 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/app/models/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "plotline" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: 5 23 | 24 | development: 25 | <<: *default 26 | database: dummy_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: dummy 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: dummy_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: dummy_production 84 | username: dummy 85 | password: <%= ENV['DUMMY_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | mount Plotline::Engine => "/plotline" 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: d115a59538da40cbb706d0a5bb1e8d93866e15706d1bd2243c8392c157d6fc2441f70169ea976fad1251f5d9041ddae016bdf6078be3b64ab625caf52e27b3f5 15 | 16 | test: 17 | secret_key_base: e277020d8b02c126c12f0658bd48d3547476d78d12dced1da79a532d7c6424338bb18ae423a38c1b9f7eaac4b475b5c61857b47dac87ebe9f7dbf134b118d985 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160224161843) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "plotline_entries", force: :cascade do |t| 20 | t.string "type" 21 | t.string "title" 22 | t.string "slug" 23 | t.text "body" 24 | t.json "payload" 25 | t.datetime "published_at" 26 | t.integer "status", default: 0 27 | t.text "tags", default: [], array: true 28 | t.integer "parent_id" 29 | t.string "checksum" 30 | t.datetime "created_at", null: false 31 | t.datetime "updated_at", null: false 32 | t.index ["parent_id"], name: "index_plotline_entries_on_parent_id", using: :btree 33 | t.index ["slug"], name: "index_plotline_entries_on_slug", using: :btree 34 | t.index ["status"], name: "index_plotline_entries_on_status", using: :btree 35 | t.index ["tags"], name: "index_plotline_entries_on_tags", using: :gin 36 | t.index ["type"], name: "index_plotline_entries_on_type", using: :btree 37 | end 38 | 39 | create_table "plotline_entry_search_data", force: :cascade do |t| 40 | t.integer "entry_id" 41 | t.string "attr_name" 42 | t.tsvector "search_data" 43 | t.text "raw_data" 44 | t.datetime "created_at", null: false 45 | t.datetime "updated_at", null: false 46 | t.index ["entry_id"], name: "index_plotline_entry_search_data_on_entry_id", using: :btree 47 | t.index ["search_data"], name: "idx_search_data", using: :gin 48 | end 49 | 50 | create_table "plotline_images", force: :cascade do |t| 51 | t.string "image" 52 | t.integer "width" 53 | t.integer "height" 54 | t.float "ratio" 55 | t.integer "file_size" 56 | t.string "content_type" 57 | t.json "exif" 58 | t.datetime "created_at" 59 | t.datetime "updated_at" 60 | end 61 | 62 | create_table "plotline_users", force: :cascade do |t| 63 | t.string "email" 64 | t.string "name" 65 | t.string "password_digest" 66 | t.string "auth_token" 67 | t.datetime "created_at", null: false 68 | t.datetime "updated_at", null: false 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pch/plotline-old/9feecd78368cdfd0088cc3d1b40dca1514535ba8/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/markdown/document.md: -------------------------------------------------------------------------------- 1 | # My gallery 2 | 3 | Photos from our last trip: 4 | 5 | --- 6 | ![Caption A](/media/img1.jpg) 7 | ![](/media/img2.jpg){#demo title=Hello} 8 | ![Caption C](/media/img3.jpg) 9 | 10 | ![](/media/img3.jpg) 11 | ![Caption B](/media/img5.jpg){#demo .fooClass .bar-baz width=100} 12 | 13 | ![](/media/img6.jpg) 14 | 15 | ![](/media/img7.jpg) 16 | ![](/media/img8.jpg) 17 | ![](/media/img9.jpg) 18 | --- 19 | 20 | * * * 21 | 22 | ## More photos 23 | 24 | And some more photos: 25 | 26 | --- 27 | ![](/media/img10.jpg) 28 | ![](/media/img11.jpg){#demo .foo .bar .baz data-important=true width=100} 29 | 30 | ![](/media/img12.jpg) 31 | --- 32 | 33 | 34 | ## Single photos 35 | 36 | ![](/media/img13.jpg) 37 | ![Demo caption](/media/img14.jpg){#demo .foo .bar data-important=true} 38 | ![Demo caption 2](/media/img14.jpg) 39 | -------------------------------------------------------------------------------- /test/fixtures/markdown/expected_output.html: -------------------------------------------------------------------------------- 1 |

My gallery

2 | 3 |

Photos from our last trip:

4 | 5 |
6 |
Caption A
Caption A
7 |
8 |
Caption C
Caption C
9 |
10 |
Caption B
Caption B
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 |

More photos

21 | 22 |

And some more photos:

23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 |

Single photos

32 | 33 |

34 |
Demo caption
Demo caption
35 |
Demo caption 2
Demo caption 2

36 | -------------------------------------------------------------------------------- /test/fixtures/plotline/entries.yml: -------------------------------------------------------------------------------- 1 | 2 | sample: 3 | type: BlogPost 4 | title: "Hello World" 5 | slug: hello-world 6 | -------------------------------------------------------------------------------- /test/integration/navigation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NavigationTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /test/models/plotline/concerns/searchable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class FooSearchableContent < Entry 5 | content_attr :body, :text 6 | content_attr :url, :string 7 | end 8 | 9 | class EntryTest < ActiveSupport::TestCase 10 | test "searchable_attributes" do 11 | FooSearchableContent.searchable_attributes(:foo_attr, :bar_attr) 12 | assert_equal [:foo_attr, :bar_attr], FooSearchableContent.searchable_attributes 13 | end 14 | 15 | test "search_attributes" do 16 | FooSearchableContent.searchable_attributes(:title, :body) 17 | 18 | foo = FooSearchableContent.new(title: "Hello", body: "World") 19 | assert_equal({ title: "Hello", body: "World"}, foo.search_attributes) 20 | end 21 | 22 | test "update_search_index" do 23 | FooSearchableContent.searchable_attributes(:title, :body) 24 | 25 | assert_difference('Plotline::EntrySearchData.count', 2) do 26 | FooSearchableContent.create(title: "Hello", body: "World") 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/models/plotline/entry_search_data_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class EntrySearchDataTest < ActiveSupport::TestCase 5 | setup do 6 | BlogPost.searchable_attributes(:title, :body) 7 | 8 | @post1 = BlogPost.create(title: "Hello World", body: "This is an example blog post about something") 9 | @post2 = BlogPost.create(title: "Hello Again", body: "Yet another example of full-text search") 10 | end 11 | 12 | test "index_entry_data" do 13 | assert_equal 2, @post1.search_data.size 14 | 15 | assert_no_difference('Plotline::EntrySearchData.count') do 16 | @post1.update_attribute(:title, "Hello World!!!") 17 | 18 | assert_equal "Hello World!!!", @post1.search_data.find_by(attr_name: 'title').raw_data 19 | end 20 | end 21 | 22 | test "search" do 23 | assert_equal [@post1], BlogPost.search('hello world') 24 | assert_equal [@post1, @post2], BlogPost.search('hello') 25 | assert_equal [@post1, @post2], BlogPost.search('example') 26 | assert_equal [@post2], BlogPost.search('full-text') 27 | assert_equal [@post1], BlogPost.search('blog posts') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/models/plotline/entry_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class FooContent < Entry; end 5 | 6 | class EntryTest < ActiveSupport::TestCase 7 | test "content_attr" do 8 | assert_equal({}, FooContent.content_attributes) 9 | 10 | FooContent.content_attr(:foo_attr, :text) 11 | FooContent.content_attr(:bar_attr, :integer) 12 | 13 | assert_equal :text, FooContent.content_attributes[:foo_attr] 14 | assert_equal :integer, FooContent.content_attributes[:bar_attr] 15 | 16 | foo = FooContent.new 17 | post = BlogPost.new 18 | 19 | assert foo.respond_to?(:foo_attr) 20 | assert !post.respond_to?(:foo_attr) 21 | 22 | foo.foo_attr = "Hello" 23 | 24 | assert_equal "Hello", foo.foo_attr 25 | assert_equal "Hello", foo.payload["foo_attr"] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/models/plotline/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class UserTest < ActiveSupport::TestCase 5 | # test "the truth" do 6 | # assert true 7 | # end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/plotline_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PlotlineTest < ActiveSupport::TestCase 4 | test "truth" do 5 | assert_kind_of Module, Plotline 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/presenters/entry_presenter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Plotline 4 | class EntryPresenterTest < ActionView::TestCase 5 | test "custom markdown parsing" do 6 | @entry = plotline_entries(:sample) 7 | @entry.body = File.read(File.join('test', 'fixtures', 'markdown', 'document.md')) 8 | expected_output = File.read(File.join('test', 'fixtures', 'markdown', 'expected_output.html')) 9 | 10 | presenter = Plotline::EntryPresenter.new(@entry, view) 11 | 12 | assert_equal expected_output, presenter.body_markdown 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)] 6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) 7 | require "rails/test_help" 8 | require "mocha/mini_test" 9 | 10 | # Filter out Minitest backtrace while allowing backtrace from other libraries 11 | # to be shown. 12 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new 13 | 14 | # Load support files 15 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 16 | 17 | # Load fixtures from the engine 18 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 19 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 20 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path 21 | ActiveSupport::TestCase.fixtures :all 22 | end 23 | 24 | class BlogPost < Plotline::Entry 25 | content_attr :body 26 | end 27 | 28 | Plotline.configure do |config| 29 | config.content_classes = %w(BlogPost) 30 | end 31 | --------------------------------------------------------------------------------