campbellhay-bureau 4.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/campbellhay-bureau.gemspec +14 -0
  4. data/gempush +8 -0
  5. data/lib/_static/_test.txt +1 -0
  6. data/lib/_static/css/main.css +586 -0
  7. data/lib/_static/css/plugin.asmselect.css +63 -0
  8. data/lib/_static/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png +0 -0
  9. data/lib/_static/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png +0 -0
  10. data/lib/_static/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png +0 -0
  11. data/lib/_static/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png +0 -0
  12. data/lib/_static/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png +0 -0
  13. data/lib/_static/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png +0 -0
  14. data/lib/_static/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png +0 -0
  15. data/lib/_static/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png +0 -0
  16. data/lib/_static/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png +0 -0
  17. data/lib/_static/css/ui-darkness/images/ui-icons_222222_256x240.png +0 -0
  18. data/lib/_static/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png +0 -0
  19. data/lib/_static/css/ui-darkness/images/ui-icons_a83300_256x240.png +0 -0
  20. data/lib/_static/css/ui-darkness/images/ui-icons_cccccc_256x240.png +0 -0
  21. data/lib/_static/css/ui-darkness/images/ui-icons_ffffff_256x240.png +0 -0
  22. data/lib/_static/css/ui-darkness/jquery-ui-1.9.1.custom.css +378 -0
  23. data/lib/_static/css/ui-darkness/jquery-ui-1.9.1.custom.min.css +5 -0
  24. data/lib/_static/img/backend_small_icons.png +0 -0
  25. data/lib/_static/img/btn.png +0 -0
  26. data/lib/_static/img/btn_old.png +0 -0
  27. data/lib/_static/img/file.png +0 -0
  28. data/lib/_static/img/handle.png +0 -0
  29. data/lib/_static/img/help/clone-button.png +0 -0
  30. data/lib/_static/img/help/create-button.png +0 -0
  31. data/lib/_static/img/help/delete-button.png +0 -0
  32. data/lib/_static/img/help/delete-warning.png +0 -0
  33. data/lib/_static/img/help/document.png +0 -0
  34. data/lib/_static/img/help/edit-button.png +0 -0
  35. data/lib/_static/img/help/form.png +0 -0
  36. data/lib/_static/img/help/image-dimensions.png +0 -0
  37. data/lib/_static/img/help/list.png +0 -0
  38. data/lib/_static/img/help/logout.png +0 -0
  39. data/lib/_static/img/help/move-button.png +0 -0
  40. data/lib/_static/img/help/overview.png +0 -0
  41. data/lib/_static/img/help/preview-button.png +0 -0
  42. data/lib/_static/img/help/relationships.png +0 -0
  43. data/lib/_static/img/help/reload-button.png +0 -0
  44. data/lib/_static/img/help/save-or-cancel.png +0 -0
  45. data/lib/_static/img/help/search.png +0 -0
  46. data/lib/_static/img/help/sub-section.png +0 -0
  47. data/lib/_static/img/help/x-list.png +0 -0
  48. data/lib/_static/img/placeholder.nutshell.jpg +0 -0
  49. data/lib/_static/img/placeholder.stash_thumb.gif +0 -0
  50. data/lib/_static/img/small-loader.gif +0 -0
  51. data/lib/_static/img/underwood_btn.png +0 -0
  52. data/lib/_static/js/addon.timepicker.js +20 -0
  53. data/lib/_static/js/jquery-ui-1.9.1.custom.js +8364 -0
  54. data/lib/_static/js/jquery-ui-1.9.1.custom.min.js +6 -0
  55. data/lib/_static/js/main.js +347 -0
  56. data/lib/_static/js/plugin.asmselect.js +407 -0
  57. data/lib/_static/js/plugin.form.js +23 -0
  58. data/lib/_static/js/plugin.mapolygon.js +3 -0
  59. data/lib/_static/js/plugin.quickmask.js +68 -0
  60. data/lib/_static/js/plugin.quicksearch.js +1 -0
  61. data/lib/_static/js/plugin.speechify.js +4 -0
  62. data/lib/_static/js/plugin.underwood.js +4 -0
  63. data/lib/_templates/_test.txt +1 -0
  64. data/lib/_templates/head.html +23 -0
  65. data/lib/_templates/help.html +104 -0
  66. data/lib/_templates/layout.html +13 -0
  67. data/lib/bureau/adapter.rb +114 -0
  68. data/lib/bureau.rb +122 -0
  69. data/lib/mongo_bureau.rb +214 -0
  70. data/lib/sequel_bureau_adapter.rb +188 -0
  71. data/test/spec_bureau.rb +123 -0
  72. data/test/spec_bureau_adapter.rb +71 -0
  73. 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 &quot;Logout&quot; 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'>&copy; 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
@@ -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