campbellhay-bureau 4.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/campbellhay-bureau.gemspec +14 -0
- data/gempush +8 -0
- data/lib/_static/_test.txt +1 -0
- data/lib/_static/css/main.css +586 -0
- data/lib/_static/css/plugin.asmselect.css +63 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-icons_222222_256x240.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-icons_a83300_256x240.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-icons_cccccc_256x240.png +0 -0
- data/lib/_static/css/ui-darkness/images/ui-icons_ffffff_256x240.png +0 -0
- data/lib/_static/css/ui-darkness/jquery-ui-1.9.1.custom.css +378 -0
- data/lib/_static/css/ui-darkness/jquery-ui-1.9.1.custom.min.css +5 -0
- data/lib/_static/img/backend_small_icons.png +0 -0
- data/lib/_static/img/btn.png +0 -0
- data/lib/_static/img/btn_old.png +0 -0
- data/lib/_static/img/file.png +0 -0
- data/lib/_static/img/handle.png +0 -0
- data/lib/_static/img/help/clone-button.png +0 -0
- data/lib/_static/img/help/create-button.png +0 -0
- data/lib/_static/img/help/delete-button.png +0 -0
- data/lib/_static/img/help/delete-warning.png +0 -0
- data/lib/_static/img/help/document.png +0 -0
- data/lib/_static/img/help/edit-button.png +0 -0
- data/lib/_static/img/help/form.png +0 -0
- data/lib/_static/img/help/image-dimensions.png +0 -0
- data/lib/_static/img/help/list.png +0 -0
- data/lib/_static/img/help/logout.png +0 -0
- data/lib/_static/img/help/move-button.png +0 -0
- data/lib/_static/img/help/overview.png +0 -0
- data/lib/_static/img/help/preview-button.png +0 -0
- data/lib/_static/img/help/relationships.png +0 -0
- data/lib/_static/img/help/reload-button.png +0 -0
- data/lib/_static/img/help/save-or-cancel.png +0 -0
- data/lib/_static/img/help/search.png +0 -0
- data/lib/_static/img/help/sub-section.png +0 -0
- data/lib/_static/img/help/x-list.png +0 -0
- data/lib/_static/img/placeholder.nutshell.jpg +0 -0
- data/lib/_static/img/placeholder.stash_thumb.gif +0 -0
- data/lib/_static/img/small-loader.gif +0 -0
- data/lib/_static/img/underwood_btn.png +0 -0
- data/lib/_static/js/addon.timepicker.js +20 -0
- data/lib/_static/js/jquery-ui-1.9.1.custom.js +8364 -0
- data/lib/_static/js/jquery-ui-1.9.1.custom.min.js +6 -0
- data/lib/_static/js/main.js +347 -0
- data/lib/_static/js/plugin.asmselect.js +407 -0
- data/lib/_static/js/plugin.form.js +23 -0
- data/lib/_static/js/plugin.mapolygon.js +3 -0
- data/lib/_static/js/plugin.quickmask.js +68 -0
- data/lib/_static/js/plugin.quicksearch.js +1 -0
- data/lib/_static/js/plugin.speechify.js +4 -0
- data/lib/_static/js/plugin.underwood.js +4 -0
- data/lib/_templates/_test.txt +1 -0
- data/lib/_templates/head.html +23 -0
- data/lib/_templates/help.html +104 -0
- data/lib/_templates/layout.html +13 -0
- data/lib/bureau/adapter.rb +114 -0
- data/lib/bureau.rb +122 -0
- data/lib/mongo_bureau.rb +214 -0
- data/lib/sequel_bureau_adapter.rb +188 -0
- data/test/spec_bureau.rb +123 -0
- data/test/spec_bureau_adapter.rb +71 -0
- metadata +126 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
<div class='help-page'>
|
2
|
+
|
3
|
+
<style type="text/css" media="all">
|
4
|
+
.help-page {
|
5
|
+
background-color: #ffffff; padding: 2em;
|
6
|
+
font-size: 16px; line-height: 1.3;
|
7
|
+
}
|
8
|
+
.help-page h1 { margin-top: 2em; }
|
9
|
+
.help-page img { width: 50%; }
|
10
|
+
</style>
|
11
|
+
|
12
|
+
<h2>Help</h2>
|
13
|
+
|
14
|
+
|
15
|
+
<h1>Overview</h1>
|
16
|
+
|
17
|
+
<p>As you can see on the following image, the screen is divided in 2 parts. On the right the menu, which will always be here, and on the right the section you are currently working on.</p>
|
18
|
+
|
19
|
+
<p><img src="/admin/_static/img/help/overview.png" alt="Overview"></p>
|
20
|
+
|
21
|
+
<p>At the top of the menu, you can see who you are logged-in as and then the sections you can work on. This usually reflects as much as possible the sections or pages of your website.</p>
|
22
|
+
|
23
|
+
<h1>Sub-Sections</h1>
|
24
|
+
|
25
|
+
<p>If you click on one of the sections in the menu and get a page like the following, this is a sub-section.</p>
|
26
|
+
|
27
|
+
<p><img src="/admin/_static/img/help/sub-section.png" alt="Sub-section"></p>
|
28
|
+
|
29
|
+
<p>This will list the sub-sections you can work on. Just click on the sub-section you wish to work on and follow the journey.</p>
|
30
|
+
|
31
|
+
<h1>List Pages</h1>
|
32
|
+
|
33
|
+
<p>When you have reached a page like the following, this is a list page.</p>
|
34
|
+
|
35
|
+
<p><img src="/admin/_static/img/help/list.png" alt="List"></p>
|
36
|
+
|
37
|
+
<p>This can be a list of articles, a list of users or a list of contact details. The presentation will always be similar.</p>
|
38
|
+
|
39
|
+
<p><img src="/admin/_static/img/help/document.png" alt="Document"></p>
|
40
|
+
|
41
|
+
<p>You will see a relevant title, a relevant image (if there is one), and buttons for actions.</p>
|
42
|
+
|
43
|
+
<p>The "edit" button is the most important. It allows you to edit the information of this entry.</p>
|
44
|
+
|
45
|
+
<p><img src="/admin/_static/img/help/edit-button.png" alt="Edit Button"></p>
|
46
|
+
|
47
|
+
<p>The "delete" button allows you to delete the entry.</p>
|
48
|
+
|
49
|
+
<p><img src="/admin/_static/img/help/delete-button.png" alt="Delete Button"></p>
|
50
|
+
|
51
|
+
<p>While being irreversible and potentially dangerous, the Bureau will warn you and give you the opportunity to cancel before proceeding.</p>
|
52
|
+
|
53
|
+
<p><img src="/admin/_static/img/help/delete-warning.png" alt="Delete Warning"></p>
|
54
|
+
|
55
|
+
<p>This symbol with 3 spots is a handle. It lets you re-order the documents by drag-and-drop.</p>
|
56
|
+
|
57
|
+
<p><img src="/admin/_static/img/help/move-button.png" alt="Delete Warning"></p>
|
58
|
+
|
59
|
+
<p>The "clone" button allows you to create another entry with the same information.</p>
|
60
|
+
|
61
|
+
<p><img src="/admin/_static/img/help/clone-button.png" alt="Clone Button"></p>
|
62
|
+
|
63
|
+
<p>Most of the time you may want to create an entry from scratch. The "create" button at the top of the page will let you do this.</p>
|
64
|
+
|
65
|
+
<p><img src="/admin/_static/img/help/create-button.png" alt="Create Button"></p>
|
66
|
+
|
67
|
+
<p>There you can also reload the page:</p>
|
68
|
+
|
69
|
+
<p><img src="/admin/_static/img/help/reload-button.png" alt="Reload Button"></p>
|
70
|
+
|
71
|
+
<p>Or filter entries to find what you are looking for:</p>
|
72
|
+
|
73
|
+
<p><img src="/admin/_static/img/help/search.png" alt="Search"></p>
|
74
|
+
|
75
|
+
<p>Entries will sometimes have other items attached to them. For example an article can have a list of images. Or Teams can then have a list of team members. These are called relationships.</p>
|
76
|
+
|
77
|
+
<p>When this is the case, you will have links for each relationship at the bottom of the entries and when you click on them, the system will bring you to another list page which follow the same structure.</p>
|
78
|
+
|
79
|
+
<p><img src="/admin/_static/img/help/relationships.png" alt="Relationships"></p>
|
80
|
+
|
81
|
+
<p>Here this project has 3 tasks and 2 collaborators. You can click on any of these links to manage the tasks or the collaborators.</p>
|
82
|
+
|
83
|
+
<h1>Form</h1>
|
84
|
+
|
85
|
+
<p>Once you get to finally edit something, the page will be a form like the example below.</p>
|
86
|
+
|
87
|
+
<p><img src="/admin/_static/img/help/form.png" alt="Form"></p>
|
88
|
+
|
89
|
+
<p>This page will probably be quite familiar to you. Field names within forms are devised to be as explicit as possible. For example they also include ideal image dimensions.</p>
|
90
|
+
|
91
|
+
<p><img src="/admin/_static/img/help/image-dimensions.png" alt="Image Dimensions"></p>
|
92
|
+
|
93
|
+
<p>At the end of the form, you can either save or cancel.</p>
|
94
|
+
|
95
|
+
<p><img src="/admin/_static/img/help/save-or-cancel.png" alt="Save or Cancel"></p>
|
96
|
+
|
97
|
+
<h1>Logout</h1>
|
98
|
+
|
99
|
+
<p>At the bottom of the menu you have a "Logout" button so that you can securely finish your working session.</p>
|
100
|
+
|
101
|
+
<p><img src="/admin/_static/img/help/logout.png" alt="Logout"></p>
|
102
|
+
|
103
|
+
</div>
|
104
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
<head>%s</head>
|
4
|
+
<body>
|
5
|
+
<div id='sidebar'>
|
6
|
+
<div id='greetings'>Welcome back,<br />%s</div>
|
7
|
+
<div id='menu'>%s</div>
|
8
|
+
%s
|
9
|
+
<div id='copyright'>© 2011-12<br />Created by <a href='http://www.campbellhay.com' target='_blank'>Campbell Hay</a></div>
|
10
|
+
</div>
|
11
|
+
<div id='slides'><div id='content' class='slide'><div class='slide-inner'><noscript><div>Please enable Javascript to get a fully working Bureau</div></noscript>%s</div></div></div>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Bureau
|
2
|
+
module Adapter
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
attr_accessor :bureau_config, :list_options
|
6
|
+
|
7
|
+
def list_view(r)
|
8
|
+
"Override Me -- #{self.name}#list_view"
|
9
|
+
end
|
10
|
+
|
11
|
+
def foreign_key_name(plural=false); "id#{'s' if plural}_"+self.name; end
|
12
|
+
def human_name; self.name.gsub(/([A-Z])/, ' \1')[1..-1]; end
|
13
|
+
def human_plural_name; human_name+'s'; end
|
14
|
+
def list_title; human_plural_name; end
|
15
|
+
|
16
|
+
def list_view_header
|
17
|
+
o = @list_options
|
18
|
+
out = "<h2 class='list-title slide-title'><span>#{list_title}</span><div class='nut-tree-toolbar'>"
|
19
|
+
out << command_plus unless bureau_config[:no_plus]
|
20
|
+
out << command_reload
|
21
|
+
out << command_search unless bureau_config[:no_search]
|
22
|
+
out << "</div></h2>\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
def sortable_on_that_page?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def command_reload
|
30
|
+
"<a class='btn btn-reload' href='#' title='Reload'></a>\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
def command_plus
|
34
|
+
o = @list_options
|
35
|
+
path = "#{o[:path]}/#{self.name}?_no_wrap=true&_destination=#{::Rack::Utils::escape(o[:destination])}"
|
36
|
+
o[:filter].each{|k,v|path<<"&model[#{k}]=#{::Rack::Utils::escape(v)}"} unless o[:filter].nil?
|
37
|
+
"<a href='#{path}' class='btn btn-plus push-stack' title='Create'></a>\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
def command_search
|
41
|
+
o = @list_options
|
42
|
+
"<form action='#{o[:destination]}' method='GET' class='search'>Search:<input type='search' name='q' value='#{o[:request]['q']}' /><input type='submit' value='Search' /></form>"
|
43
|
+
end
|
44
|
+
|
45
|
+
def many_to_many_picker
|
46
|
+
opts = @list_options
|
47
|
+
klass = bureau_config[:minilist_class].is_a?(Symbol) ? Kernel.const_get(bureau_config[:minilist_class]) : bureau_config[:minilist_class]
|
48
|
+
klass.list_options = @list_options
|
49
|
+
params_sample = (opts[:filter]||{}).map{|k,v| "model[#{k}]=#{::Rack::Utils::escape(v)}" }
|
50
|
+
params_sample << "model[#{klass.foreign_key_name}]="
|
51
|
+
o = "<div class='many-to-many-picker' rel='#{params_sample.join('&')}'>\n"
|
52
|
+
o << "<div class='many-to-many-search'>Filter:<input type='search' class='minisearch' name='minisearch' /> Drag and Drop what you want to add</div>\n"
|
53
|
+
o << "<div class='minilist-wrapper'>\n"
|
54
|
+
o << klass.minilist_view
|
55
|
+
o << "</div>\n"
|
56
|
+
o << "</div>\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
|
63
|
+
def nutshell_header
|
64
|
+
o = model.list_options
|
65
|
+
out = "<div class='nutshell-header'><div class='nutshell-title' title='#{self.to_label}'>#{self.to_label}</div>"
|
66
|
+
out << "<div class='sortable-handle btn' title='Drag Me'></div>\n" if o[:sortable]
|
67
|
+
out << "</div>\n"
|
68
|
+
end
|
69
|
+
def in_nutshell
|
70
|
+
o = model.list_options
|
71
|
+
out = "<div class='in-nutshell'>\n"
|
72
|
+
out << self.to_bureau_thumb('nutshell.jpg') if self.respond_to?(:to_bureau_thumb)
|
73
|
+
out << "</div>\n"
|
74
|
+
end
|
75
|
+
def nutshell_toolbar
|
76
|
+
o = model.list_options
|
77
|
+
class_path = "#{o[:path]}/#{model.name}"
|
78
|
+
path = "#{class_path}/#{self.id}"
|
79
|
+
out = "<div class='nutshell-toolbar'>Tools:"
|
80
|
+
out << "#{self.backend_delete_form(path, :destination=>o[:destination])}<div class='btn btn-delete' title='Delete'></div>\n" unless model.bureau_config[:no_delete]
|
81
|
+
out << "#{self.backend_clone_form(class_path, :destination=>o[:destination])}<div class='btn btn-clone' title='Clone'></div>\n" unless (model.bureau_config[:no_plus]||model.bureau_config[:no_clone])
|
82
|
+
out << "<a href='#{preview_on_frontend}#{preview_on_frontend.match(/\?/) ? '&' : '?'}_preview=true' class='btn btn-preview' target='_blank' title='Preview'></a>\n" unless preview_on_frontend.nil?
|
83
|
+
out << "<a href='#{path}?_no_wrap=true&_destination=#{::Rack::Utils::escape(o[:destination])}' class='btn btn-edit push-stack' title='Edit'></a>\n" unless model.bureau_config[:no_edit]
|
84
|
+
out << "</div>\n"
|
85
|
+
end
|
86
|
+
def nutshell_children; ''; end
|
87
|
+
|
88
|
+
def to_nutshell
|
89
|
+
out = "<li class='nutshell nutshell-#{model.name}' id='#{model.name}-#{self.id}' data-scene-selector-coordinates='#{self.scene_selector_coordinates if self.respond_to?(:scene_selector_coordinates)}'>"
|
90
|
+
out << nutshell_header
|
91
|
+
out << in_nutshell
|
92
|
+
out << nutshell_toolbar
|
93
|
+
out << nutshell_children
|
94
|
+
out << "</li>"
|
95
|
+
end
|
96
|
+
|
97
|
+
def placeholder_thumb(size)
|
98
|
+
o = model.list_options
|
99
|
+
"<img src='#{o[:path]}/_static/img/placeholder.#{size.gsub(/^(.*)_([a-zA-Z]+)$/, '\1.\2')}' />\n"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Override the clone column list from RackBackendAPI
|
103
|
+
def cloning_backend_columns
|
104
|
+
model.respond_to?(:stash) ? (default_backend_columns - model.stash_reflection.keys) : default_backend_columns
|
105
|
+
end
|
106
|
+
# Default list of columns for nutshell
|
107
|
+
def nutshell_backend_columns; default_backend_columns; end
|
108
|
+
# Meant to be ovveridden with a link to see the entry in the frontend if applicable
|
109
|
+
def preview_on_frontend; nil; end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
data/lib/bureau.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'rack/golem'
|
2
|
+
|
3
|
+
module Bureau
|
4
|
+
F = ::File
|
5
|
+
DIR = F.expand_path(F.dirname(__FILE__))
|
6
|
+
BEFORE = Proc.new{
|
7
|
+
if @r.fullpath.sub(/\/$/, '')==_config[:path]&&_config[:index]
|
8
|
+
@action, *@action_arguments = _config[:index]
|
9
|
+
end
|
10
|
+
if @r['_no_wrap']
|
11
|
+
@action = "wrap_api_response"
|
12
|
+
@api_arguments = @action_arguments
|
13
|
+
@action_arguments = []
|
14
|
+
if @r['_destination'].nil?
|
15
|
+
dest = "&_destination=#{::Rack::Utils::escape(@r.fullpath)}"
|
16
|
+
@r.env.update({'QUERY_STRING'=>@r.env['QUERY_STRING']+dest})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.included(klass)
|
22
|
+
klass.class_eval do
|
23
|
+
extend ClassMethods
|
24
|
+
include InstanceMethods
|
25
|
+
include Rack::Golem
|
26
|
+
end
|
27
|
+
klass.before(&BEFORE)
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def new(*); ::Rack::Static.new(super, :urls => ["/_static"], :root => DIR); end
|
32
|
+
def read(p); F.read("%s/_templates/%s" % [DIR,p]); end
|
33
|
+
def config
|
34
|
+
@config ||= {
|
35
|
+
:client_name => 'Client Name',
|
36
|
+
:website_url => 'www.domain.com',
|
37
|
+
:path => '/admin',
|
38
|
+
:logout_path => '/admin/logout', # sometimes higher in stack
|
39
|
+
:menu => [[['Home', '/']]],
|
40
|
+
:head_addons => '',
|
41
|
+
:index => nil
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods
|
47
|
+
|
48
|
+
def list(m); @content = Kernel.const_get(m).list_view(@r); _finish; end
|
49
|
+
#def edit(m,id); @content = eval(m)[id].backend_form(_config[:path]+'/'+m+'/'+id); _finish; end
|
50
|
+
|
51
|
+
def submenu(group,section)
|
52
|
+
title, *links = _config[:menu][group.to_i][section.to_i]
|
53
|
+
@content = "<h2 class='slide-title'><span>#{title}</span></h2>"
|
54
|
+
@content << "<ul class='submenu-list'>\n"
|
55
|
+
until links.empty?
|
56
|
+
@content << "<li><a class='push-stack' href='#{links.shift}'>#{links.shift}</a></li>\n"
|
57
|
+
end
|
58
|
+
@content << "</ul>\n"
|
59
|
+
_finish
|
60
|
+
end
|
61
|
+
|
62
|
+
def help
|
63
|
+
@content = _t('help.html')
|
64
|
+
_finish
|
65
|
+
end
|
66
|
+
|
67
|
+
def wrap_api_response
|
68
|
+
status, header, res = @app.call(@r.env)
|
69
|
+
@res.status = status
|
70
|
+
@res.header.replace(header)
|
71
|
+
@content = res.body.inject(''){|r,s| r+s }
|
72
|
+
_with_layout
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def _t(p); self.class.read(p); end # read template
|
78
|
+
|
79
|
+
def _finish; xhr? ? @content : _with_layout; end
|
80
|
+
|
81
|
+
def _with_layout # Take @content and wrap it in the layout
|
82
|
+
admin = (@r.env['rack.session'] || {})['cerberus_user']
|
83
|
+
greetings = admin.nil? ? '' : admin.tr('_-', ' ').upcase
|
84
|
+
logout_btn = "<a class='logout' href='#{_config[:logout_path]}'>Logout</a>"
|
85
|
+
_t('layout.html') % [_head, greetings, _menu, logout_btn, @content]
|
86
|
+
end
|
87
|
+
|
88
|
+
def _config; @cms_config ||= self.class.config.update(:path=>@r.script_name).dup; end # Instance config
|
89
|
+
|
90
|
+
def _menu
|
91
|
+
o = "<ul id='menu-list'>\n"
|
92
|
+
_config[:menu].each_with_index do |group,group_i|
|
93
|
+
o << "<ul class='menu-group-list'>\n"
|
94
|
+
group.each_with_index do |section,section_i|
|
95
|
+
title, *links = section
|
96
|
+
selected = links.include?(@r.env['REQUEST_URI']) || @r.env['REQUEST_URI'][/\/submenu\/#{group_i}\/#{section_i}$/]
|
97
|
+
o << "<li class='menu-section #{selected&&'selected-menu-section'||''}'>"
|
98
|
+
url = links.size==1 ? links[0] : "#{_config[:path]}/submenu/#{group_i}/#{section_i}"
|
99
|
+
o << "<a href='#{url}'>#{title}</a>"
|
100
|
+
o << "</li>\n"
|
101
|
+
end
|
102
|
+
o << "</ul>\n"
|
103
|
+
end
|
104
|
+
o << "</ul>\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
def _head
|
108
|
+
p = _config[:path]
|
109
|
+
_t('head.html') % [
|
110
|
+
p,p,p, # CSS
|
111
|
+
p,p,p,p,p,p,p,p,p,p,p, # JS
|
112
|
+
_config[:head_addons]
|
113
|
+
]
|
114
|
+
end
|
115
|
+
|
116
|
+
def xhr?; @r.xhr?||@r['_xhr']=='true'; end
|
117
|
+
|
118
|
+
def bureau_before; instance_eval(&Bureau::BEFORE); end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/lib/mongo_bureau.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
begin
|
2
|
+
require 'mongo_mutation'
|
3
|
+
require 'mongo_crushyform'
|
4
|
+
require 'mongo_stash'
|
5
|
+
rescue LoadError
|
6
|
+
nil
|
7
|
+
end
|
8
|
+
require 'bureau/adapter'
|
9
|
+
|
10
|
+
module BackendApiAdapter
|
11
|
+
module ClassMethods
|
12
|
+
def backend_get(id); id=='unique' ? find_one : get(id); end
|
13
|
+
def backend_post(doc=nil); inst = new(doc); inst.is_new = true; inst; end
|
14
|
+
end
|
15
|
+
module InstanceMethods
|
16
|
+
def backend_delete; delete; end
|
17
|
+
def backend_put(fields); update_doc(fields); end
|
18
|
+
def backend_values; @doc; end
|
19
|
+
def backend_save?; !save.nil?; end
|
20
|
+
def backend_form(url, cols=nil, opts={})
|
21
|
+
cols ||= default_backend_columns
|
22
|
+
if block_given?
|
23
|
+
fields_list = ''
|
24
|
+
yield(fields_list)
|
25
|
+
else
|
26
|
+
fields_list = respond_to?(:crushyform) ? crushyform(cols) : backend_fields(cols)
|
27
|
+
end
|
28
|
+
o = "<form action='#{url}' method='POST' #{"enctype='multipart/form-data'" if fields_list.match(/type='file'/)} class='backend-form'>\n"
|
29
|
+
o << backend_form_title unless block_given?
|
30
|
+
o << fields_list
|
31
|
+
opts[:method] = 'PUT' if (opts[:method].nil? && !self.new?)
|
32
|
+
o << "<input type='hidden' name='_method' value='#{opts[:method]}' />\n" unless opts[:method].nil?
|
33
|
+
o << "<input type='hidden' name='_destination' value='#{opts[:destination]}' />\n" unless opts[:destination].nil?
|
34
|
+
o << "<input type='hidden' name='_submit_text' value='#{opts[:submit_text]}' />\n" unless opts[:submit_text].nil?
|
35
|
+
o << "<input type='hidden' name='_no_wrap' value='#{opts[:no_wrap]}' />\n" unless opts[:no_wrap].nil?
|
36
|
+
cols.each do |c|
|
37
|
+
o << "<input type='hidden' name='fields[]' value='#{c}' />\n"
|
38
|
+
end
|
39
|
+
o << "<input type='submit' name='save' value='#{opts[:submit_text] || 'SAVE'}' />\n"
|
40
|
+
o << "</form>\n"
|
41
|
+
o
|
42
|
+
end
|
43
|
+
def backend_delete_form(url, opts={}); backend_form(url, [], {:submit_text=>'X', :method=>'DELETE'}.update(opts)){}; end
|
44
|
+
def backend_clone_form(url, opts={})
|
45
|
+
backend_form(url, [], {:submit_text=>'CLONE', :method=>'POST'}.update(opts)) do |out|
|
46
|
+
out << "<input type='hidden' name='clone_id' value='#{self.id}' />\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
# Silly but usable form prototype
|
50
|
+
# Not really meant to be used in a real case
|
51
|
+
# It uses a textarea for everything
|
52
|
+
# Override it
|
53
|
+
# Or even better, use Sequel-Crushyform plugin instead
|
54
|
+
def backend_fields(cols)
|
55
|
+
o = ''
|
56
|
+
cols.each do |c|
|
57
|
+
identifier = "#{id}-#{self.class}-#{c}"
|
58
|
+
o << "<label for='#{identifier}'>#{c.to_s.capitalize}</label><br />\n"
|
59
|
+
o << "<textarea id='#{identifier}' name='model[#{c}]'>#{self[c]}</textarea><br />\n"
|
60
|
+
end
|
61
|
+
o
|
62
|
+
end
|
63
|
+
def backend_form_title; self.new? ? "<h2><span>New #{model.human_name}</span></h2>\n" : "<h2><span>Edit #{self.to_label}</span></h2>\n"; end
|
64
|
+
def backend_show; 'OK'; end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module MongoBureau
|
69
|
+
|
70
|
+
BUREAU_CRUSHYFORM_TYPES = {
|
71
|
+
}
|
72
|
+
|
73
|
+
def self.included(base)
|
74
|
+
base.extend(BackendApiAdapter::ClassMethods)
|
75
|
+
base.extend(MongoCrushyform::ClassMethods) if defined?(MongoCrushyform)
|
76
|
+
base.extend(Bureau::Adapter::ClassMethods)
|
77
|
+
base.extend(ClassMethods)
|
78
|
+
base.bureau_config = {:nut_tree_class=>'sortable-grid'}
|
79
|
+
base.crushyform_types.update(BUREAU_CRUSHYFORM_TYPES)
|
80
|
+
end
|
81
|
+
|
82
|
+
module ClassMethods
|
83
|
+
attr_accessor :list_options
|
84
|
+
|
85
|
+
def list_view(r)
|
86
|
+
@list_options = {:request=>r, :destination=>r.fullpath, :path=>r.script_name, :filter=>r['filter'] }
|
87
|
+
@list_options.store(:sortable,sortable_on_that_page?)
|
88
|
+
out = list_view_header
|
89
|
+
out << scene_selector unless bureau_config[:scene_selector_class].nil?
|
90
|
+
out << many_to_many_picker unless bureau_config[:minilist_class].nil?
|
91
|
+
out << "<ul class='nut-tree #{'sortable' if @list_options[:sortable]} #{bureau_config[:nut_tree_class]}' id='#{self.name}' rel='#{@list_options[:path]}/#{self.name}'>"
|
92
|
+
self.find(typecast_filter(@list_options[:filter]||{}), (self.respond_to?(:list_view_fields) ? {fields: self.list_view_fields} : {})).each do |m|
|
93
|
+
out << m.to_nutshell
|
94
|
+
end
|
95
|
+
out << "</ul>"
|
96
|
+
end
|
97
|
+
|
98
|
+
def sortable_on_that_page?
|
99
|
+
o = @list_options
|
100
|
+
o[:search].nil? && @schema.key?('position') && (@schema['position'][:scope].nil? || (o[:filter]||{}).key?(@schema['position'][:scope]))
|
101
|
+
end
|
102
|
+
|
103
|
+
def minilist_view
|
104
|
+
o = "<ul class='minilist'>\n"
|
105
|
+
self.find.each do |m|
|
106
|
+
thumb = m.respond_to?(:to_bureau_thumb) ? m.to_bureau_thumb('stash_thumb_gif') : m.placeholder_thumb('stash_thumb_gif')
|
107
|
+
o << "<li title='#{m.to_label}' id='mini-#{m.id}'>#{thumb}<div>#{m.to_label}</div></li>\n"
|
108
|
+
end
|
109
|
+
o << "</ul>\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
def scene_selector
|
113
|
+
o = @list_options
|
114
|
+
klass = bureau_config[:scene_selector_class].is_a?(Symbol) ? Kernel.const_get(bureau_config[:scene_selector_class]) : bureau_config[:scene_selector_class]
|
115
|
+
obj = klass.get(o[:filter]["id_#{bureau_config[:scene_selector_class]}"])
|
116
|
+
unless obj.nil?
|
117
|
+
out = "<p>Point and click in order to highlight a zone.</p>\n"
|
118
|
+
out << obj.build_image_tag('image','original', :class=>'mapolygon-me')
|
119
|
+
save_btn = command_plus.sub(/btn btn-plus/, 'save-mapolygon').sub(/></, '>Create this zone<').sub(/(href='[^']*)/, "\\1&model[coordinates]=")
|
120
|
+
out << "<div class='scene-selector-toolbar'><button type='button' class='reset-mapolygon'>Reset</button> #{save_btn}</div>\n"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def image_slot(name='image',opts={})
|
127
|
+
super(name,opts)
|
128
|
+
# First image slot is considered the best bureau thumb
|
129
|
+
unless instance_methods.include?(:to_bureau_thumb)
|
130
|
+
define_method :to_bureau_thumb do |style|
|
131
|
+
generic_thumb(name, style)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def typecast_filter filter={}
|
137
|
+
filter.each do |k,v|
|
138
|
+
filter[k] = true if v=='true'
|
139
|
+
filter[k] = false if v=='false'
|
140
|
+
filter[k] = v.to_i if v[/^\d+$/]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
include BackendApiAdapter::InstanceMethods
|
147
|
+
include MongoCrushyform::InstanceMethods if defined?(MongoCrushyform)
|
148
|
+
include Bureau::Adapter::InstanceMethods
|
149
|
+
|
150
|
+
def after_stash(col)
|
151
|
+
convert(col, "-resize '100x75^' -gravity center -extent 100x75", 'stash_thumb_gif')
|
152
|
+
convert(col, "-resize '184x138^' -gravity center -extent 184x138", 'nutshell_jpg')
|
153
|
+
end
|
154
|
+
|
155
|
+
def bureau_attachment_url_for obj, col='image', size='original'
|
156
|
+
return obj.attachment_url(col,size) if obj.respond_to?(:attachment_url)
|
157
|
+
"/gridfs/#{obj.doc[col][size]}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def generic_thumb(img , size='stash_thumb_gif', obj=self)
|
161
|
+
return placeholder_thumb(size) if obj.nil?
|
162
|
+
current = obj.doc[img]
|
163
|
+
if !current.nil? && !current[size].nil?
|
164
|
+
"<img src='#{bureau_attachment_url_for(obj,img,size)}' onerror=\"this.style.display='none'\" />\n"
|
165
|
+
else
|
166
|
+
placeholder_thumb(size)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_thumb(c)
|
171
|
+
current = @doc[c]
|
172
|
+
if current.respond_to?(:[])
|
173
|
+
img_url = @doc[c]['stash_thumb_gif'].nil? ? model.list_options&&"#{model.list_options[:path]}/_static/img/file.png" : bureau_attachment_url_for(self,c,'stash_thumb_gif')
|
174
|
+
"<img src='#{img_url}' #{"width='100'" unless @doc[c]['stash_thumb_gif'].nil?} onerror=\"this.style.display='none'\" />\n"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def scene_selector_coordinates; @doc['coordinates']; end
|
179
|
+
|
180
|
+
def in_nutshell
|
181
|
+
o = model.list_options
|
182
|
+
out = "<div class='in-nutshell'>\n"
|
183
|
+
out << self.to_bureau_thumb('nutshell_jpg') if self.respond_to?(:to_bureau_thumb)
|
184
|
+
cols = model.bureau_config[:quick_update_fields] || nutshell_backend_columns.select{|col|
|
185
|
+
[:boolean,:select].include?(model.schema[col][:type]) && !model.schema[col][:multiple] && !model.schema[col][:no_quick_update]
|
186
|
+
}
|
187
|
+
cols.each do |c|
|
188
|
+
column_label = model.schema[c][:name] || c.to_s.sub(/^id_/, '').tr('_', ' ').capitalize
|
189
|
+
out << "<div class='quick-update'><form><span class='column-title'>#{column_label}:</span> #{self.crushyinput(c)}</form></div>\n"
|
190
|
+
end
|
191
|
+
out << "</div>\n"
|
192
|
+
end
|
193
|
+
|
194
|
+
def nutshell_children
|
195
|
+
o = model.list_options
|
196
|
+
out = ""
|
197
|
+
nutshell_backend_associations.each do |k, opts|
|
198
|
+
next if opts[:hidden]
|
199
|
+
k = Kernel.const_get(k)
|
200
|
+
link = "#{o[:path]}/list/#{k}?filter[#{model.foreign_key_name}]=#{self.id}"
|
201
|
+
text = opts[:link_text] || "#{k.human_name}(s)"
|
202
|
+
out << "<a href='#{link}' class='push-stack sublist-link nutshell-child'>#{text} #{self.children_count(k) unless opts[:hide_count]}</a>\n"
|
203
|
+
end
|
204
|
+
out
|
205
|
+
end
|
206
|
+
|
207
|
+
def nutshell_backend_associations
|
208
|
+
model.relationships
|
209
|
+
end
|
210
|
+
|
211
|
+
def default_backend_columns; model.schema.keys; end
|
212
|
+
def cloning_backend_columns; default_backend_columns.reject{|c| model.schema[c][:type]==:attachment}; end
|
213
|
+
|
214
|
+
end
|