outpost-cms 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.md +295 -0
- data/Rakefile +26 -0
- data/app/assets/images/glyphicons-halflings-red.png +0 -0
- data/app/assets/javascripts/outpost/application.js +1 -0
- data/app/assets/javascripts/outpost/auto_slug_field.js.coffee +53 -0
- data/app/assets/javascripts/outpost/base.js.coffee +1 -0
- data/app/assets/javascripts/outpost/date_time_input.js.coffee +108 -0
- data/app/assets/javascripts/outpost/field_counter.js.coffee +93 -0
- data/app/assets/javascripts/outpost/field_manager.js.coffee +37 -0
- data/app/assets/javascripts/outpost/global_plugins.js.coffee +87 -0
- data/app/assets/javascripts/outpost/index_manager.js.coffee +88 -0
- data/app/assets/javascripts/outpost/notification.js.coffee +46 -0
- data/app/assets/javascripts/outpost/preview.js.coffee +60 -0
- data/app/assets/javascripts/outpost/templates/date_field.jst.eco +3 -0
- data/app/assets/javascripts/outpost/templates/loading.jst.eco +11 -0
- data/app/assets/javascripts/outpost/templates/slug_generate_button.jst.eco +1 -0
- data/app/assets/javascripts/outpost/templates/time_field.jst.eco +3 -0
- data/app/assets/javascripts/outpost/templates.js +1 -0
- data/app/assets/javascripts/outpost.js +32 -0
- data/app/assets/stylesheets/outpost/_base.css.scss +127 -0
- data/app/assets/stylesheets/outpost/_edit.css.scss +13 -0
- data/app/assets/stylesheets/outpost/_forms.css.scss +116 -0
- data/app/assets/stylesheets/outpost/_index.css.scss +68 -0
- data/app/assets/stylesheets/outpost/_utility.css.scss +16 -0
- data/app/assets/stylesheets/outpost/application.css.scss +1 -0
- data/app/assets/stylesheets/outpost/bootstrap/bootstrap.css.scss +49 -0
- data/app/assets/stylesheets/outpost/bootstrap/datepicker.css.scss +301 -0
- data/app/assets/stylesheets/outpost.css.scss +14 -0
- data/app/controllers/outpost/application_controller.rb +40 -0
- data/app/controllers/outpost/base_controller.rb +3 -0
- data/app/controllers/outpost/errors_controller.rb +9 -0
- data/app/controllers/outpost/home_controller.rb +2 -0
- data/app/controllers/outpost/resource_controller.rb +12 -0
- data/app/controllers/outpost/sessions_controller.rb +36 -0
- data/app/helpers/authorization_helper.rb +44 -0
- data/app/helpers/list_helper.rb +243 -0
- data/app/helpers/outpost_helper.rb +49 -0
- data/app/helpers/render_helper.rb +41 -0
- data/app/helpers/utility_helper.rb +136 -0
- data/app/inputs/date_time_input.rb +12 -0
- data/app/models/permission.rb +18 -0
- data/app/models/user_permission.rb +4 -0
- data/app/views/kaminari/bootstrap/_first_page.html.erb +3 -0
- data/app/views/kaminari/bootstrap/_gap.html.erb +3 -0
- data/app/views/kaminari/bootstrap/_last_page.html.erb +3 -0
- data/app/views/kaminari/bootstrap/_next_page.html.erb +3 -0
- data/app/views/kaminari/bootstrap/_page.html.erb +3 -0
- data/app/views/kaminari/bootstrap/_paginator.html.erb +17 -0
- data/app/views/kaminari/bootstrap/_prev_page.html.erb +3 -0
- data/app/views/layouts/outpost/application.html.erb +101 -0
- data/app/views/layouts/outpost/minimal.html.erb +26 -0
- data/app/views/outpost/errors/error_404.html.erb +1 -0
- data/app/views/outpost/errors/error_500.html.erb +8 -0
- data/app/views/outpost/home/dashboard.html.erb +1 -0
- data/app/views/outpost/resource/_errors.html.erb +11 -0
- data/app/views/outpost/resource/_extra_fields.html.erb +1 -0
- data/app/views/outpost/resource/_form_fields.html.erb +9 -0
- data/app/views/outpost/resource/edit.html.erb +44 -0
- data/app/views/outpost/resource/index.html.erb +22 -0
- data/app/views/outpost/resource/new.html.erb +21 -0
- data/app/views/outpost/resource/search.html.erb +1 -0
- data/app/views/outpost/resource/show.html.erb +1 -0
- data/app/views/outpost/sessions/new.html.erb +16 -0
- data/app/views/outpost/shared/_add_link.html.erb +1 -0
- data/app/views/outpost/shared/_breadcrumbs.html.erb +15 -0
- data/app/views/outpost/shared/_cancel_link.html.erb +1 -0
- data/app/views/outpost/shared/_columns.html.erb +5 -0
- data/app/views/outpost/shared/_filters.html.erb +16 -0
- data/app/views/outpost/shared/_flash_messages.html.erb +6 -0
- data/app/views/outpost/shared/_form_block.html.erb +18 -0
- data/app/views/outpost/shared/_form_nav.html.erb +12 -0
- data/app/views/outpost/shared/_headers.html.erb +16 -0
- data/app/views/outpost/shared/_index_header.html.erb +4 -0
- data/app/views/outpost/shared/_list_table.html.erb +7 -0
- data/app/views/outpost/shared/_modal.html.erb +16 -0
- data/app/views/outpost/shared/_navigation.html.erb +31 -0
- data/app/views/outpost/shared/_notice.html.erb +1 -0
- data/app/views/outpost/shared/_pagination.html.erb +2 -0
- data/app/views/outpost/shared/_preview_errors.html.erb +9 -0
- data/app/views/outpost/shared/_submit_row.html.erb +50 -0
- data/config/routes.rb +4 -0
- data/lib/action_view/helpers/form_builder.rb +71 -0
- data/lib/outpost/breadcrumbs.rb +73 -0
- data/lib/outpost/config.rb +63 -0
- data/lib/outpost/controller/actions.rb +72 -0
- data/lib/outpost/controller/authentication.rb +34 -0
- data/lib/outpost/controller/authorization.rb +28 -0
- data/lib/outpost/controller/callbacks.rb +14 -0
- data/lib/outpost/controller/custom_errors.rb +41 -0
- data/lib/outpost/controller/filtering.rb +22 -0
- data/lib/outpost/controller/helpers.rb +52 -0
- data/lib/outpost/controller/ordering.rb +46 -0
- data/lib/outpost/controller/preferences.rb +71 -0
- data/lib/outpost/controller.rb +123 -0
- data/lib/outpost/engine.rb +10 -0
- data/lib/outpost/helpers/naming.rb +22 -0
- data/lib/outpost/helpers.rb +6 -0
- data/lib/outpost/hook.rb +35 -0
- data/lib/outpost/list/base.rb +78 -0
- data/lib/outpost/list/column.rb +24 -0
- data/lib/outpost/list/filter.rb +37 -0
- data/lib/outpost/list.rb +15 -0
- data/lib/outpost/model/authentication.rb +34 -0
- data/lib/outpost/model/authorization.rb +32 -0
- data/lib/outpost/model/identifier.rb +39 -0
- data/lib/outpost/model/methods.rb +23 -0
- data/lib/outpost/model/naming.rb +63 -0
- data/lib/outpost/model/routing.rb +138 -0
- data/lib/outpost/model/serializer.rb +27 -0
- data/lib/outpost/model.rb +22 -0
- data/lib/outpost/test.rb +21 -0
- data/lib/outpost/version.rb +3 -0
- data/lib/outpost-cms.rb +2 -0
- data/lib/outpost.rb +80 -0
- data/lib/tasks/outpost_tasks.rake +7 -0
- data/spec/controllers/authentication_spec.rb +62 -0
- data/spec/controllers/sessions_controller_spec.rb +99 -0
- data/spec/factories.rb +31 -0
- data/spec/helpers/authorization_helper_spec.rb +47 -0
- data/spec/helpers/list_helper_spec.rb +74 -0
- data/spec/helpers/outpost_helper_spec.rb +5 -0
- data/spec/helpers/render_helper_spec.rb +19 -0
- data/spec/helpers/utility_helper_spec.rb +53 -0
- data/spec/internal/app/controllers/application_controller.rb +3 -0
- data/spec/internal/app/controllers/outpost/people_controller.rb +23 -0
- data/spec/internal/app/controllers/outpost/pidgeons_controller.rb +5 -0
- data/spec/internal/app/controllers/people_controller.rb +9 -0
- data/spec/internal/app/controllers/pidgeons_controller.rb +3 -0
- data/spec/internal/app/models/person.rb +10 -0
- data/spec/internal/app/models/pidgeon.rb +3 -0
- data/spec/internal/app/models/post.rb +4 -0
- data/spec/internal/app/models/user.rb +4 -0
- data/spec/internal/app/views/people/index.html.erb +7 -0
- data/spec/internal/app/views/people/show.html.erb +1 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/initializers/configuration.rb +3 -0
- data/spec/internal/config/initializers/outpost.rb +6 -0
- data/spec/internal/config/routes.rb +16 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +44 -0
- data/spec/internal/db/seeds.rb +14 -0
- data/spec/internal/log/test.log +59277 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/breadcrumbs_spec.rb +54 -0
- data/spec/lib/config_spec.rb +76 -0
- data/spec/lib/controller/actions_spec.rb +5 -0
- data/spec/lib/controller/authorization_spec.rb +4 -0
- data/spec/lib/controller/callbacks_spec.rb +31 -0
- data/spec/lib/controller/helpers_spec.rb +33 -0
- data/spec/lib/controller_spec.rb +25 -0
- data/spec/lib/helpers/naming_spec.rb +10 -0
- data/spec/lib/hook_spec.rb +13 -0
- data/spec/lib/list/base_spec.rb +96 -0
- data/spec/lib/list/column_spec.rb +46 -0
- data/spec/lib/list/filter_spec.rb +44 -0
- data/spec/lib/model/authentication_spec.rb +29 -0
- data/spec/lib/model/authorization_spec.rb +66 -0
- data/spec/lib/model/identifier_spec.rb +51 -0
- data/spec/lib/model/methods_spec.rb +8 -0
- data/spec/lib/model/naming_spec.rb +55 -0
- data/spec/lib/model/routing_spec.rb +166 -0
- data/spec/lib/model/serializer_spec.rb +13 -0
- data/spec/lib/outpost_spec.rb +34 -0
- data/spec/models/permission_spec.rb +10 -0
- data/spec/models/user_permission_spec.rb +4 -0
- data/spec/spec_helper.rb +22 -0
- metadata +411 -0
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2013 Bryan Ricker
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Outpost
|
|
2
|
+
[](https://travis-ci.org/SCPR/outpost)
|
|
3
|
+
|
|
4
|
+
A Rails Engine for quickly standing up a CMS for a Newsroom.
|
|
5
|
+
|
|
6
|
+
## Dependencies
|
|
7
|
+
* `rails >= 3.2`
|
|
8
|
+
* `ruby >= 1.9.3`
|
|
9
|
+
|
|
10
|
+
See `.travis.yml` to see which Ruby versions are officially supported.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
Add `gem 'outpost-cms'` to your Gemfile. The module you interact with is just
|
|
14
|
+
`Outpost`.
|
|
15
|
+
|
|
16
|
+
**A note about the gem/repository/module name discrepancy**
|
|
17
|
+
There is [another gem](http://rubygems.org/gems/outpost) called "Outpost"
|
|
18
|
+
which occupies the same namespace as this gem. However, the other Outpost
|
|
19
|
+
is meant for service monitoring, and I can't imagine a scenario where
|
|
20
|
+
these two gems would be used together in the same application. Therefore,
|
|
21
|
+
I'm keeping the module name, and just renaming the gem to `outpost-cms`
|
|
22
|
+
so we can both exist on RubyGems.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
This gem also has some hard dependencies that aren't in the gemspec.
|
|
26
|
+
My goal is to reduce these dependencies as much as possible, but as this was
|
|
27
|
+
extracted from the KPCC application, these are fairly strict at this point.
|
|
28
|
+
|
|
29
|
+
* `simple_form` - for Rails 3.2, use `~> 2.1.0`.
|
|
30
|
+
For Rails 4.0, you'll need to use `~> 3.0.0.beta1`
|
|
31
|
+
* `kaminari` - You need to use the
|
|
32
|
+
[kaminari master branch](https://github.com/amatsuda/kaminari).
|
|
33
|
+
* `eco`
|
|
34
|
+
* `sass-rails`
|
|
35
|
+
* `bootstrap-sass`
|
|
36
|
+
* `coffee-rails`
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
### Authentication
|
|
40
|
+
Much like Devise, Outpost provides a basic `SessionsController` and
|
|
41
|
+
corresponding views. To use these, just add them to your routes:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
namespace :outpost do
|
|
45
|
+
resources :sessions, only: [:create, :destroy]
|
|
46
|
+
get 'login' => "sessions#new", as: :login
|
|
47
|
+
get 'logout' => "sessions#destroy", as: :logout
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Outpost also provides the `Outpost::Model::Authentication` module,
|
|
52
|
+
which you should include into your User model to work with the provided
|
|
53
|
+
`SessionsController`:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class User < ActiveRecord::Base
|
|
57
|
+
include Outpost::Model::Authentication
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your User class should have at least the following methods:
|
|
62
|
+
* `password_digest` (string)
|
|
63
|
+
* `last_login` (datetime)
|
|
64
|
+
* `can_login` (boolean)
|
|
65
|
+
* `is_superuser` (boolean)
|
|
66
|
+
* `name` (string)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#### Configuration
|
|
70
|
+
|
|
71
|
+
You can set a different User class, or the attribute which the user
|
|
72
|
+
should use to login:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Outpost::Config.configure do |config|
|
|
76
|
+
config.user_class = "AdminUser"
|
|
77
|
+
config.authentication_attribute = :username
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
##### Routes
|
|
83
|
+
|
|
84
|
+
Note that this gem doesn't provide any routes for you. It is expected that you'll want to use the path globbing to render Outpost-style 404's inside of Outpost (rather than your application's 404 page), so it gets too messy and complicated to try to combine your routes with path globbing.
|
|
85
|
+
|
|
86
|
+
You'll want to put this at the bottom of you `outpost` namespace in your routes:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
root to: 'home#dashboard'
|
|
90
|
+
|
|
91
|
+
resources :sessions, only: [:create, :destroy]
|
|
92
|
+
get 'login' => "sessions#new", as: :login
|
|
93
|
+
get 'logout' => "sessions#destroy", as: :logout
|
|
94
|
+
|
|
95
|
+
get "*path" => 'errors#not_found'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### Authorization
|
|
100
|
+
Outpost comes with a built-in `Permission` model, whose only attribute is
|
|
101
|
+
a String `resource`, which stores a class name which you want to be
|
|
102
|
+
authorized throughout the application. Run this migration to set it up:
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
create_table :permissions do |t|
|
|
107
|
+
t.string :resource
|
|
108
|
+
t.timestamps
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
create_table :user_permissions do |t|
|
|
112
|
+
t.integer :user_id
|
|
113
|
+
t.integer :permission_id
|
|
114
|
+
t.timestamps
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
add_index :permissions, :resource
|
|
118
|
+
add_index :user_permissions, :user_id
|
|
119
|
+
add_index :user_permissions, :permission_id
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can include `Outpost::Model::Authorization` into your User model
|
|
123
|
+
to provide the Permission association, and also add the `can_manage?`
|
|
124
|
+
method:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
if !current_user.can_manage?(Post)
|
|
128
|
+
redirect_to outpost_root_path, alert: "Not Authorized"
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Authorization is "All-or-None"... in other words, a user can either
|
|
133
|
+
manage a resource or not - A user with permission for a particular model
|
|
134
|
+
is able to Create, Read, Update, and Delete any of those objects.
|
|
135
|
+
|
|
136
|
+
Outpost controllers will automatically authorize their resource. Within
|
|
137
|
+
views, you can use one of the provided helpers to guard a block of text
|
|
138
|
+
or a link:
|
|
139
|
+
|
|
140
|
+
```erb
|
|
141
|
+
<%= guard Post do %>
|
|
142
|
+
Only users who are authorized for Posts will see this.
|
|
143
|
+
<% end %>
|
|
144
|
+
|
|
145
|
+
<%= guarded_link_to Post, "Linked if authorized, plaintext if not", posts_path %>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
### User Preferences
|
|
150
|
+
Preferences are stored in the session, and on a per-resource basis.
|
|
151
|
+
Outpost provides built-in hooks in the controller and views for
|
|
152
|
+
Order (attribute) and Sort Mode ("asc", "desc"). In order to manage other
|
|
153
|
+
preferences, you'll want to make use of a handful of methods that get
|
|
154
|
+
mixed-in to your Outpost controllers:
|
|
155
|
+
|
|
156
|
+
* `preference` - Access a preference's value.
|
|
157
|
+
* `set_preference` - Set a preference's value.
|
|
158
|
+
* `unset_preference` - Unset a preference's value.
|
|
159
|
+
|
|
160
|
+
The key for a preference needs to follow the convention:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
"#{model.content_key}_#{preference}"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For example:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
set_preference("blog_entries_color", "ff0000")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
You also need to add the parameter that the preference is using to
|
|
173
|
+
`config.preferences`:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
Outpost::Config.configure do |config|
|
|
177
|
+
# ...
|
|
178
|
+
config.preferences += [:color]
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A resource-based preference is automatically cleared if its param is an empty string (not `nil`). For example:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# GET /outpost/posts?color=ff0000
|
|
186
|
+
set_preference('posts_color', params[:color])
|
|
187
|
+
preference('posts_color') # => ff0000
|
|
188
|
+
|
|
189
|
+
# GET /outpost/posts?color=
|
|
190
|
+
preference('posts_color') # => nil
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If you have a preference for a non-resourceful page, you need to manage its
|
|
194
|
+
cleanup manually.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
## Javascripts
|
|
199
|
+
|
|
200
|
+
Outpost comes with a bunch of useful scripts built-in. Some of them are automatically used, and some are provided as "opt-in" functionality.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
### Field Counter
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
This will add a counter above any field which will show the number of characters entered into that field, the target length, and the +/- fuzziness, as well as a color indicating where in that range they are.
|
|
208
|
+
|
|
209
|
+
#### Use
|
|
210
|
+
|
|
211
|
+
Add the class `field-counter` to a div wrapping the input field, and two data-attributes containing integers:
|
|
212
|
+
|
|
213
|
+
* `data-target` - The target length (default: 145)
|
|
214
|
+
* `data-fuzziness` - The fuzziness allowed (default: 20)
|
|
215
|
+
|
|
216
|
+
If you're using `simple_form`, it might look like this:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
f.input :title, wrapper_html; { class: "field-counter", data: { target: 50, fuzziness: 10} }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
### Preview
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
The Javascript for Preview is what handles sending the form data to the server, but you'll need to handle the server-side stuff yourself. The "Preview" button will show up once you've added a `preview` action to that controller.
|
|
228
|
+
|
|
229
|
+
#### Use
|
|
230
|
+
|
|
231
|
+
The `preview` action needs to do a few things:
|
|
232
|
+
|
|
233
|
+
* Find the object from the passed-in `obj_key` (You can use `Outpost::obj_by_key`). You'll also need to handle what happens if the record hasn't been saved yet.
|
|
234
|
+
* Merge in the changed attributes.
|
|
235
|
+
* Render the proper template/layout, or any validation errors (using `render_preview_validation_errors`).
|
|
236
|
+
* Make sure you don't save anything. For this, I recommend doing any object updating inside of a database transaction, because assigning associations to a persisted object will save the object. Outpost provides a controller method, `with_rollback`, which will perform the block inside of a database transaction and force an `ActiveRecord::Rollback` at the end.
|
|
237
|
+
|
|
238
|
+
Here is a full example of what your `preview` action could look like:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
def preview
|
|
242
|
+
@post = ContentBase.obj_by_key(params[:obj_key]) || Post.new
|
|
243
|
+
|
|
244
|
+
with_rollback @post do
|
|
245
|
+
@post.assign_attributes(form_params)
|
|
246
|
+
|
|
247
|
+
if @post.valid?
|
|
248
|
+
render "/posts/_post", layout: "application", locals: { post: @post }
|
|
249
|
+
else
|
|
250
|
+
render_preview_validation_errors(@post)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
You'll also need to add two routes for the preview action:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
resources :posts do
|
|
260
|
+
put "preview", on: :member
|
|
261
|
+
post "preview", on: :collection
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
You need both `post` and `put` to allow the preview to happen from either the New or Edit pages. If you're using Rails 4, use `patch` instead of `put`. In fact, if you're using Rails 4 (or the `routing_concerns` gem), then you can use Routing Concerns:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
concern :previewable do
|
|
269
|
+
patch "preview", on: :member
|
|
270
|
+
post "preview", on: :collection
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
resources :posts, concerns: [:previewable]
|
|
274
|
+
resources :reporters, concerns: [:previewable]
|
|
275
|
+
resources :stories, concerns: [:previewable]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
#### More documentation to come.
|
|
280
|
+
|
|
281
|
+
## Todo
|
|
282
|
+
A ton of stuff. Here is a sampler:
|
|
283
|
+
|
|
284
|
+
* Generators for resources (models, controllers).
|
|
285
|
+
* Add record versioning (needs to be extracted from the SCPRv4 app).
|
|
286
|
+
* Documentation... oh man, the documentation...
|
|
287
|
+
|
|
288
|
+
## Contributing
|
|
289
|
+
Pull Requests are encouraged! This engine was built specifically for KPCC,
|
|
290
|
+
so its flexibility is limited... if you have improvements to make, please
|
|
291
|
+
make them.
|
|
292
|
+
|
|
293
|
+
Fork it, make your changes, and send me a pull request.
|
|
294
|
+
|
|
295
|
+
Run tests with `bundle exec rake test`
|
data/Rakefile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
RAKED = true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
#require 'rdoc/task'
|
|
6
|
+
require 'rspec/core/rake_task'
|
|
7
|
+
require 'combustion'
|
|
8
|
+
|
|
9
|
+
# RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
# rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
# rdoc.title = 'Outpost'
|
|
12
|
+
# rdoc.markup = 'tomdoc'
|
|
13
|
+
# rdoc.options << '--line-numbers'
|
|
14
|
+
# rdoc.rdoc_files.include('README.md')
|
|
15
|
+
# rdoc.rdoc_files.include('lib/**/*.rb')
|
|
16
|
+
# rdoc.rdoc_files.include('app/helpers/**/*.rb')
|
|
17
|
+
# rdoc.rdoc_files.include('app/models/**/*.rb')
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each { |f| load f }
|
|
21
|
+
|
|
22
|
+
Bundler.require :default, :test
|
|
23
|
+
Combustion.initialize! :active_record, :action_controller
|
|
24
|
+
Combustion::Application.load_tasks
|
|
25
|
+
|
|
26
|
+
RSpec::Core::RakeTask.new(:test)
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= require outpost
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Find slug fields and load them up
|
|
2
|
+
$ ->
|
|
3
|
+
for field in $("form input[name*='[slug]']")
|
|
4
|
+
new outpost.AutoSlugField(field: field)
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# AutoSlugField
|
|
8
|
+
#
|
|
9
|
+
# Takes a field and turns it into a slug on-the-fly
|
|
10
|
+
#
|
|
11
|
+
class outpost.AutoSlugField
|
|
12
|
+
DefaultOptions:
|
|
13
|
+
titleClass: ".sluggable"
|
|
14
|
+
maxLength: 50
|
|
15
|
+
|
|
16
|
+
constructor: (options={}) ->
|
|
17
|
+
@options = _.defaults options, @DefaultOptions
|
|
18
|
+
|
|
19
|
+
# Find the sluggable title field - if it doesn't
|
|
20
|
+
# exist then we can't auto-generate a slug
|
|
21
|
+
@titleField = $(@options.titleClass)[0]
|
|
22
|
+
|
|
23
|
+
if @titleField
|
|
24
|
+
@slugField = $ @options.field
|
|
25
|
+
@maxLength = @options.maxLength
|
|
26
|
+
@button = $ JST['outpost/templates/slug_generate_button']()
|
|
27
|
+
|
|
28
|
+
# If we found a matching field,
|
|
29
|
+
# render the generate button and add it after the slug field
|
|
30
|
+
@slugField.after(@button)
|
|
31
|
+
@button.on
|
|
32
|
+
click: (event) =>
|
|
33
|
+
@updateSlug($(@titleField).val())
|
|
34
|
+
event.preventDefault()
|
|
35
|
+
false
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
|
|
39
|
+
#------------------
|
|
40
|
+
|
|
41
|
+
updateSlug: (value) ->
|
|
42
|
+
@slugField.val @slugify(value)
|
|
43
|
+
|
|
44
|
+
#------------------
|
|
45
|
+
|
|
46
|
+
slugify: (str) ->
|
|
47
|
+
str.toLowerCase()
|
|
48
|
+
.replace(/\s+/g, "-") # Spaces -> `-`
|
|
49
|
+
.replace(/-{2,}/g, '-') # Fix accidental double-hyphens
|
|
50
|
+
.replace(/[^\w\-]+/g, '') # Remove non-word characters/hyphen
|
|
51
|
+
.replace(/^-+/, '') # Trim hyphens from beginning
|
|
52
|
+
.substring(0, @maxLength) # Just the first 50 characters
|
|
53
|
+
.replace(/-+$/, '') # Trim hyphens from end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
window.outpost ?= {}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Find the inputs and load them.
|
|
2
|
+
$ ->
|
|
3
|
+
outpost.DateTimeInput.buildDateTimeInputs($("form"))
|
|
4
|
+
outpost.DateTimeInput.buildDateInputs($("form"))
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# DateTimeInput
|
|
8
|
+
#
|
|
9
|
+
# Turns simple textfields (for datetime attributes) into
|
|
10
|
+
# awesome timetime picker things.
|
|
11
|
+
#
|
|
12
|
+
# Does both Date and Time inputs (separately)
|
|
13
|
+
#
|
|
14
|
+
class outpost.DateTimeInput
|
|
15
|
+
@buildDateTimeInputs: (els) ->
|
|
16
|
+
for wrapper in $("div.datetime", els)
|
|
17
|
+
new outpost.DateTimeInput(wrapper: wrapper)
|
|
18
|
+
|
|
19
|
+
@buildDateInputs: (els) ->
|
|
20
|
+
for wrapper in $("div.date", els)
|
|
21
|
+
new outpost.DateTimeInput(wrapper: wrapper, time: false, field: "input.date")
|
|
22
|
+
|
|
23
|
+
#-----------------------
|
|
24
|
+
|
|
25
|
+
DefaultOptions:
|
|
26
|
+
time: true
|
|
27
|
+
dateTemplate: JST["outpost/templates/date_field"]
|
|
28
|
+
timeTemplate: JST["outpost/templates/time_field"]
|
|
29
|
+
timestampEls: ".timestamp-el"
|
|
30
|
+
populateIcons: "span.populate"
|
|
31
|
+
controls: "div.controls"
|
|
32
|
+
field: "input.datetime"
|
|
33
|
+
dateFormat: "YYYY-MM-DD"
|
|
34
|
+
timeFormat: "HH:mm"
|
|
35
|
+
dbFormat: "YYYY-MM-DD HH:mm:ss"
|
|
36
|
+
|
|
37
|
+
constructor: (options={}) ->
|
|
38
|
+
@options = _.defaults options, @DefaultOptions
|
|
39
|
+
@time = @options.time
|
|
40
|
+
|
|
41
|
+
# Elements
|
|
42
|
+
@wrapper = $ @options.wrapper
|
|
43
|
+
@controls = $ @options.controls, @wrapper
|
|
44
|
+
@field = $ @options.field, @wrapper
|
|
45
|
+
|
|
46
|
+
# Attributes
|
|
47
|
+
@id = @field.attr("id")
|
|
48
|
+
@dateId = "#{@id}_date"
|
|
49
|
+
@timeId = "#{@id}_time"
|
|
50
|
+
|
|
51
|
+
# Hide the field since we don't want anybody editing it directly
|
|
52
|
+
@field.hide()
|
|
53
|
+
|
|
54
|
+
# Render the templates
|
|
55
|
+
# Prepend time first so it's second
|
|
56
|
+
@controls.prepend(@options.timeTemplate(time_id: @timeId, time_format: @options.timeFormat)) if @time
|
|
57
|
+
@controls.prepend(@options.dateTemplate(date_id: @dateId, date_format: @options.dateFormat))
|
|
58
|
+
|
|
59
|
+
# Register the newly-created elements
|
|
60
|
+
@timestampEls = $ @options.timestampEls, @wrapper
|
|
61
|
+
@dateEl = $ "##{@dateId}"
|
|
62
|
+
@timeEl = $ "##{@timeId}"
|
|
63
|
+
@populateIcons = $ @options.populateIcons, @wrapper
|
|
64
|
+
|
|
65
|
+
# Fill in the new fields with the correct date/time
|
|
66
|
+
# Only if the field has a value (i.e. we're editing the object)
|
|
67
|
+
if @field.val()
|
|
68
|
+
@dateEl.val @getDate(@options.dateFormat)
|
|
69
|
+
@timeEl.val @getDate(@options.timeFormat) if @time
|
|
70
|
+
|
|
71
|
+
# Make the dateEl a datepicker
|
|
72
|
+
@dateEl.datepicker(autoclose: true, format: @options.dateFormat.toLowerCase())
|
|
73
|
+
|
|
74
|
+
# Fill in hidden text field when visible field is changed
|
|
75
|
+
@timestampEls.on
|
|
76
|
+
change: (event) => (@field.trigger "update")
|
|
77
|
+
|
|
78
|
+
@field.on
|
|
79
|
+
update: => (@setDate(@dateEl.val(), @timeEl.val() if @time))
|
|
80
|
+
|
|
81
|
+
# Fill in visible fields with right now time,
|
|
82
|
+
# and trigger the "update" event on field hidden field
|
|
83
|
+
@populateIcons.on
|
|
84
|
+
click: (event) =>
|
|
85
|
+
@populateDate(@dateEl, @options.dateFormat)
|
|
86
|
+
@populateDate(@timeEl, @options.timeFormat) if @time
|
|
87
|
+
@field.trigger "update"
|
|
88
|
+
|
|
89
|
+
# Populate a visible field with a date human-readable date string
|
|
90
|
+
populateDate: (el, format) ->
|
|
91
|
+
date = moment().format(format)
|
|
92
|
+
el.val(date)
|
|
93
|
+
|
|
94
|
+
# Get timestamp from hidden field
|
|
95
|
+
getDate: (format) ->
|
|
96
|
+
date = Date.parse(@field.val())
|
|
97
|
+
moment(date).format(format) if date
|
|
98
|
+
|
|
99
|
+
# Set value of hidden field to a real date
|
|
100
|
+
setDate: (date, time="") ->
|
|
101
|
+
date = moment(Date.parse("#{date} #{time}"))
|
|
102
|
+
console.log "date set to", date
|
|
103
|
+
|
|
104
|
+
if date
|
|
105
|
+
formatted = date.format(@options.dbFormat)
|
|
106
|
+
@field.val(formatted)
|
|
107
|
+
else
|
|
108
|
+
@field.val("")
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
$ ->
|
|
3
|
+
# Initialize a FieldCounter for any field that asks for it.
|
|
4
|
+
for field in $("form .field-counter")
|
|
5
|
+
el = $(field)
|
|
6
|
+
target = el.attr("data-target")
|
|
7
|
+
fuzziness = el.attr("data-fuzziness")
|
|
8
|
+
new outpost.FieldCounter(el, target: target, fuzziness: fuzziness)
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# FieldCounter
|
|
12
|
+
# To turn a field into a field counter, add three attributes:
|
|
13
|
+
#
|
|
14
|
+
# class="field-counter"
|
|
15
|
+
# data-target="50" # Perfect length
|
|
16
|
+
# data-fuzziness="10" # Lee-way in either direction (inclusive)
|
|
17
|
+
#
|
|
18
|
+
# Anything spanning the range of
|
|
19
|
+
#
|
|
20
|
+
# `(target - fuzziness) through (target + fuzziness)`
|
|
21
|
+
#
|
|
22
|
+
# (inclusive) will be considered "in-range". Everything else is
|
|
23
|
+
# "out of range". By default, this class will use the Twitter Bootstrap
|
|
24
|
+
# notification classes, but that can be overridden.
|
|
25
|
+
#
|
|
26
|
+
class outpost.FieldCounter
|
|
27
|
+
DefaultOptions:
|
|
28
|
+
target: 145 # Perfect length
|
|
29
|
+
fuzziness: 20 # Lee-way (in either direction, inclusive)
|
|
30
|
+
inRangeClass: "alert alert-success"
|
|
31
|
+
outOfRangeClass: "alert alert-warning"
|
|
32
|
+
counterClass: "counter-notify"
|
|
33
|
+
counterWrapper: ".controls" # The element to which the counter will be prepended
|
|
34
|
+
counterStyle: "padding: 3px; margin: 0 0 2px 0;"
|
|
35
|
+
|
|
36
|
+
constructor: (@el, options={}) ->
|
|
37
|
+
@options = _.defaults options, @DefaultOptions
|
|
38
|
+
|
|
39
|
+
# Setup elements
|
|
40
|
+
@field = $("input, textarea", @el)
|
|
41
|
+
@counterEl = $("<div />", class: @options.counterClass, style: @options.counterStyle)
|
|
42
|
+
$(@options.counterWrapper, @el).prepend @counterEl
|
|
43
|
+
|
|
44
|
+
# Setup attributes
|
|
45
|
+
@count = 0
|
|
46
|
+
@target = parseInt(@options.target)
|
|
47
|
+
@fuzziness = parseInt(@options.fuzziness)
|
|
48
|
+
@rangeLow = @target - @fuzziness
|
|
49
|
+
@rangeHigh = @target + @fuzziness
|
|
50
|
+
|
|
51
|
+
@inRangeClass = @options.inRangeClass
|
|
52
|
+
@outOfRangeClass = @options.outOfRangeClass
|
|
53
|
+
|
|
54
|
+
# Register listeners
|
|
55
|
+
@field.on
|
|
56
|
+
keyup: (event) =>
|
|
57
|
+
@updateCount($(event.target).val().length)
|
|
58
|
+
|
|
59
|
+
@el.on
|
|
60
|
+
updateCounter: (event, count) =>
|
|
61
|
+
@updateText(count)
|
|
62
|
+
@updateColor(count)
|
|
63
|
+
|
|
64
|
+
# Set the count on initialize
|
|
65
|
+
@updateCount(@field.val().length)
|
|
66
|
+
|
|
67
|
+
#--------------
|
|
68
|
+
|
|
69
|
+
inRange: ->
|
|
70
|
+
@rangeLow <= @count and @count <= @rangeHigh
|
|
71
|
+
|
|
72
|
+
#--------------
|
|
73
|
+
|
|
74
|
+
updateCount: (length) ->
|
|
75
|
+
@count = length
|
|
76
|
+
@el.trigger "updateCounter", @count
|
|
77
|
+
|
|
78
|
+
#--------------
|
|
79
|
+
|
|
80
|
+
updateText: (count) ->
|
|
81
|
+
@counterEl.html("<strong>Optimal Length:</strong> #{count} of #{@target} (+/- #{@fuzziness})")
|
|
82
|
+
|
|
83
|
+
#--------------
|
|
84
|
+
|
|
85
|
+
updateColor: (count) ->
|
|
86
|
+
if @inRange()
|
|
87
|
+
@counterEl.removeClass(@outOfRangeClass)
|
|
88
|
+
@counterEl.addClass(@inRangeClass)
|
|
89
|
+
else
|
|
90
|
+
@counterEl.removeClass(@inRangeClass)
|
|
91
|
+
@counterEl.addClass(@outOfRangeClass)
|
|
92
|
+
|
|
93
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
##
|
|
2
|
+
# FieldManager
|
|
3
|
+
#
|
|
4
|
+
# A simple class that listens for certain events
|
|
5
|
+
# and does things to forms
|
|
6
|
+
#
|
|
7
|
+
class outpost.FieldManager
|
|
8
|
+
constructor: ->
|
|
9
|
+
|
|
10
|
+
$("fieldset.form-block legend").on
|
|
11
|
+
click: (event) ->
|
|
12
|
+
target = $(@)
|
|
13
|
+
target.siblings(".fields").toggle()
|
|
14
|
+
target.siblings(".notification").toggle()
|
|
15
|
+
|
|
16
|
+
# Add fields
|
|
17
|
+
$(".js-add-fields").on
|
|
18
|
+
click: (event) ->
|
|
19
|
+
event.preventDefault()
|
|
20
|
+
|
|
21
|
+
target = $(@)
|
|
22
|
+
time = new Date().getTime()
|
|
23
|
+
regexp = new RegExp(target.data('id'), 'g')
|
|
24
|
+
fields = $(target.data('fields').trim().replace(regexp, time))
|
|
25
|
+
|
|
26
|
+
if buildTarget = target.data('build-target')
|
|
27
|
+
$(buildTarget).append fields
|
|
28
|
+
else
|
|
29
|
+
target.before(fields)
|
|
30
|
+
|
|
31
|
+
# Build any special fields.
|
|
32
|
+
# TODO: Can we accomplish this with triggers?
|
|
33
|
+
outpost.DateTimeInput.buildDateTimeInputs(fields)
|
|
34
|
+
outpost.DateTimeInput.buildDateInputs(fields)
|
|
35
|
+
$("select", fields).select2
|
|
36
|
+
placeholder: " "
|
|
37
|
+
allowClear: true
|