There is a simple javascript build with old js but u can use it in ext 2.0 right
1 var grid;
2 var ds;
3 // shorthand alias
4 // shorthand alias
5 var fm = Ext.form, Ed = Ext.grid.GridEditor;
6
7 ds = new Ext.data.Store({
8 proxy: new Ext.data.HttpProxy({url: '/admin/products/data'}),
9
10 reader: new Ext.data.JsonReader({
11 root: 'Products',
12 totalProperty: 'Total',
13 id: 'id'
14 }, [ {name: 'code'},
15 {name: 'category'},
16 {name: 'sub_categories'},
17 {name: 'name'},
18 {name: 'discount'},
19 {name: 'price'},
20 {name: 'quantity'},
21 {name: 'active'} ]),
22 // turn off remote sorting
23 remoteSort: false
24 });
25
26 function formatBoolean(value){
27 return value ? '<span style="color:green">Yes</span>' : '<span style="color:red">No</span>';
28 };
29
30 function formatQuantity(value){
31 if (value <= 5) {
32 return "<span style=\"color:red\">"+value+"</span>"
33 } else if (value > 5 && value < 10) {
34 return "<span style=\"color:orange\">"+value+"</span>"
35 } else if (value > 10){
36 return "<span style=\"color:green\">"+value+"</span>"
37 }
38 };
39
40 function formatPercentage(value){
41 return value + "%"
42 };
43
44 var cm = new Ext.grid.ColumnModel
45 ([{
46 id: 'code',
47 header: 'Code',
48 dataIndex: 'code',
49 width: 80
50 },{
51 id: 'category',
52 header: 'Category',
53 dataIndex: 'category',
54 width: 55
55 },{
56 id: 'sub_categories',
57 header: 'Sub Categories',
58 dataIndex: 'sub_categories',
59 width: 100
60 },{
61 id: 'name',
62 header: "Name",
63 dataIndex: 'name',
64 width: 250
65 },{
66 header: 'Discount',
67 dataIndex: 'discount',
68 width: 60,
69 renderer:formatPercentage,
70 editor: new Ed(new fm.NumberField({
71 allowBlank: false,
72 allowNegative: false,
73 allowDecimals: true
74 }))
75 },{
76 header: 'Price',
77 dataIndex: 'price',
78 width: 100,
79 renderer: Ext.util.Format.gbMoney,
80 editor: new Ed(new fm.NumberField({
81 allowBlank: false,
82 allowNegative: false,
83 allowDecimals: true
84 }))
85 },{
86 header: 'Quantity',
87 dataIndex: 'quantity',
88 width: 60,
89 renderer:formatQuantity,
90 editor: new Ed(new fm.NumberField({
91 allowBlank: false,
92 allowNegative: false,
93 allowDecimals: false
94 }))
95 },{
96 header: 'Active',
97 dataIndex: 'active',
98 width: 60,
99 renderer: formatBoolean,
100 editor: new Ed(new fm.Checkbox())
101 }]);
102
103 cm.defaultSortable = true;
104
105 grid = new Ext.grid.EditorGrid('products-ct', {
106 ds: ds,
107 cm: cm,
108 selModel: new Ext.grid.RowSelectionModel({singleSelect:true}),
109 autoExpandColumn: 'name',
110 clicksToEdit:1
111 });
112
113 var onAfterEditGrid = function(e) {
114 query = 'id='+e.record.id+'&product['+e.field+']='+e.value
115 GridPanel.el.mask('Sending data to server...', 'x-mask-loading');
116 new Ajax.Request('/admin/products/update',
117 {method:'post', parameters:query, onSuccess:unmask, onFailure:unmask});
118 };
119
120 grid.on('afteredit', onAfterEditGrid);
121
And on my controller
1 product = Product.find(params[:id])
2
3 if product.update_attributes(params[:product])
4 render :json => { :success => true, :msg => 'Saved', :data => {} }
5 else
6 render :json => { :success => true, :msg => '#{product.show_errors}', :data => {} }
7 end
This is a starting point for make a tree with the extjs (v 1.0) framework.
1 # app/model/category.rb
2 class Category < ActiveRecord::Base
3 validates_presence_of :name
4
5 belongs_to :image
6
7 has_many :categorizations, :dependent => :destroy
8 has_many :products, :through => :categorizations
9
10 acts_as_sluggable :with => :name
11
12 acts_as_nested_set
13
14 ...
In the view a script like that
1 EditableCategories = function(){
2 return {
3 init : function(){
4 // turn on quick tips
5 Ext.QuickTips.init();
6 // seeds for the new node suffix
7 var cseed = 0, oseed = 0;
8 // create the primary toolbar
9
10 var tb = new Ext.Toolbar('toolbar');
11 tb.add({
12 id:'add',
13 text: 'Add',
14 disabled:false,
15 handler: add,
16 cls: 'x-btn-text-icon add',
17 tooltip: 'Add a new Menu'
18 },'-',{
19 id:'remove',
20 text:'Delete',
21 disabled:false,
22 handler:removeNode,
23 cls:'x-btn-text-icon remove',
24 tooltip:'Delete the Menu'
25 });
26
27 layout = new Ext.BorderLayout("main", {
28 north: {
29 split:false,
30 titlebar: false
31 },
32 west: {
33 split:false,
34 initialSize: 200,
35 border: ''
36 },
37 center: {
38 split:false,
39 autoScroll:true,
40 tabPosition:'top',
41 toolbar: tb
42 }
43 });
44
45 layout.beginUpdate();
46
47 layout.add('north', new Ext.ContentPanel('toolbar', 'Header'));
48 layout.add('west', new Ext.ContentPanel('content-items', {title: 'Menu', fitToFrame:true}));
49 layout.add('center', new Ext.ContentPanel('form', {title: 'Contentuto', closable: false}));
50
51 layout.restoreState();
52 layout.endUpdate();
53
54 Lipsiadmin.getLayout().add("center", new Ext.NestedLayoutPanel(layout));
55
56 // for enabling and disabling
57 var btns = tb.items.map;
58
59 // editable ctree
60 var xt = Ext.tree;
61
62 var ctree = new xt.TreePanel('content-items', {
63 animate:true,
64 loader: new xt.TreeLoader({dataUrl:'/admin/categories/data'}),
65 enableDD:true,
66 containerScroll: true
67 });
68
69 ctree.el.addKeyListener(Ext.EventObject.DELETE, removeNode);
70
71 // set the root node
72 var croot = new xt.AsyncTreeNode({
73 text: 'Home',
74 draggable:false,
75 id:'source'
76 });
77
78 ctree.setRootNode(croot);
79
80 // render the ctree
81 ctree.render();
82 croot.expand(true);
83
84 ctree.el.on('keypress', function(e){
85 if(e.isNavKeyPress()){
86 e.stopEvent();
87 }
88 });
89
90 // when the tree selection changes, enable/disable the toolbar buttons
91 var sm = ctree.getSelectionModel();
92 sm.on('selectionchange', function(){
93 var n = sm.getSelectedNode();
94 if(!n || !n.parentNode){
95 btns.remove.disable();
96 return;
97 } else {
98 btns.remove.enable();
99 return;
100 }
101 });
102
103 ctree.on('click', function(node){click(node)});
104 ctree.on('enddrag', save);
105
106 // create the editor for the component ctree
107 var ge = new xt.TreeEditor(ctree, {
108 allowBlank:false,
109 blankText:'You must insert a name!',
110 selectOnFocus:true
111 });
112
113 ge.on('complete', save);
114
115 // add component handler
116 function add(){
117 var id = guid('c-');
118 var text = 'Menu '+(++cseed);
119 var node = new xt.TreeNode({
120 text: text,
121 iconCls:'folder',
122 cls:'folder',
123 type:'folder',
124 allowDelete:true,
125 allowEdit:true
126 });
127 croot.appendChild(node);
128 node.expand(false, false);
129 node.select();
130 ge.triggerEdit(node);
131 }
132
133 // remove handler
134 function removeNode(){
135 var n = sm.getSelectedNode();
136 if(n){
137 Ext.get('main').mask('Sending data to server...', 'x-mask-loading');
138 new Ajax.Request('/admin/categories/destroy',
139 {method:'post', parameters:'id='+n.id, onSuccess:refresh, onFailure:refresh});
140 }
141 }
142
143 function getChildren(c){
144 var ch = [];
145
146 if (c.parentNode){
147 var cld = {
148 id: c.id,
149 text:c.text,
150 parent_id: c.parentNode.id
151 }
152 }
153
154 ch.push(cld)
155
156 if (c.childrenRendered){
157 c.eachChild(function(o){
158 getChildren(o).each(function(z){ch.push(z);});
159 });
160 }
161
162 return ch;
163 }
164
165 // save to the server in a format usable in ruby
166 function save(){
167 croot.expand(true) // WHY I NEED TO DO THAT? U KNOW A BETTER WAY?
168 var ch = getChildren(croot);
169
170 Ext.get('main').mask('Sending data to server...', 'x-mask-loading');
171
172 new Ajax.Request('/admin/categories/update',
173 {method:'post', parameters:'data='+encodeURIComponent(Ext.encode(ch)), onSuccess:refresh, onFailure:refresh});
174
175 }
176
177 function refresh(){
178 Ext.get('main').unmask();
179 croot.reload();
180 croot.expand(true);
181 }
182
183 // semi unique ids across edits
184 function guid(prefix){
185 return prefix+(new Date().getTime());
186 }
187
188 function click(node){
189 new Ajax.Request('/admin/categories/load_details/'+node.id, {asynchronous:true, evalScripts:true});
190 }
191 },
192
193 refresh : function(id){
194 new Ajax.Request('/admin/categories/load_details/'+id, {asynchronous:true, evalScripts:true});
195 }
196 }
197 }();
198
199 Ext.onReady(EditableCategories.init, EditableCategories, true);
Finally for get records in the controller some similar to this:
1 def data
2 categories = Category.find_by_sql("select * from categories where parent_id is null")
3 data = get_tree(categories,nil)
4 render :text => data.to_json, :layout=>false
5 end
6
7 def get_tree(categories, parent)
8 data = Array.new
9 categories.each { |category|
10 if !category.leaf?
11 if data.empty?
12 data = [{"text" => category.name, "id" => category.id, "leaf" => false,
13 "children" => get_tree(category.children,category) }]
14 else
15 data.concat([{"text" => category.name, "id" => category.id, "leaf" => false,
16 "children" => get_tree(category.children,category)}])
17 end
18 else
19 data.concat([{"text" => category.name, "id" => category.id, "cls" => "folder", "leaf" => false, "children" => []}])
20 end
21 }
22 return data
23 end
There you can find some short tutorial that show you how you can extend Lipsiadmin fatures
Paperclip is a super simply and powerful plugin for manage uploads.
The author say:
For some reason, file attachment is annoying. I don’t know why, and I know a lot of people have attempted to solve the problem in the past, myself included. Yet it still is. Having gotten fed up with gotchas and design decisions that we didn’t agree with, I went and wrote Paperclip on the plane to RailsConf last year. We’ve been using it here in various forms since and IMHO it’s the way to handle uploads, and finally decided that it should be released.
Let's know how use it:
1 class User < ActiveRecord::Base
2 has_attached_file :avatar,
3 :styles => { :square => ["64x64#", :png],
4 :small => "150x150>" }
5 end
Styles are an hash of thumbnail styles and their geometries. You can find more about geometry strings at the ImageMagick website (www.imagemagick.org/script/command-line-options.php#resize). Paperclip also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally inside the dimensions and then crop the rest off (weighted at the center). The default value is to generate no thumbnails.
For make an avatar for user we can do:
script/generate attachment user avatar
1 class AddAvatarToUser < ActiveRecord::Migration
2 def self.up
3 add_column :users, :avatar_file_name, :string
4 add_column :users, :avatar_content_type, :string
5 add_column :users, :avatar_file_size, :integer
6 end
7
8 def self.down; ...; end
9 end
Now with lipsiadmin we can simply do for generate our views: script/generate lipsiadmin_page user -i avatar
Remember that -i can accept an array of image like: lipsiadmin_page -i image1,image2,image3
We also make a view for file like pdf so we have also: lipsiadmin_page -l file1,file2
For more details see revision r35
The view are like:
1 <% form_for :user, :html => { :multipart => true } do |form| %>
2 <%= form.file_field :avatar %>
3 <% end %>
And for show it we can do:
1 <%= image_tag @user.avatar.url %>
2 <%= image_tag @user.avatar.url(:medium) %>
3 <%= image_tag @user.avatar.url(:thumb) %>
Super simple yea?
For manage accounts of this admin we use a library based on the beautifull plugin of Rick Olson
We can manage Admin User and Site User.
I written a new <%= error_messages_for :account > now called <= ext_error_messages_for :account %> for show in a better way errors.
<a href="http://rails.lipsiasoft.com/uploads/LipsiaAdmin12.png" rel="lightbox"><img src="http://rails.lipsiasoft.com/uploads/LipsiaAdmin12-tm.png" style="border:1px solid #507AAA" /></a>
I included in ActiveRecord a new method for show errors in new Ext Popup
1 module LipsiaSoft
2 module BetterErrors
3 def show_errors
4 return "- " + self.errors.full_messages.join("<br />- ")
5 end
6 end
7 end
This library is for manage Ext tree (see howto) in this way:
1 def get_tree(categories, parent)
2 data = Array.new
3 categories.each { |category|
4 if !category.leaf?
5 if data.empty?
6 data = [{"text" => category.name, "id" => category.id, "leaf" => false,
7 "children" => get_tree(category.children,category) }]
8 else
9 data.concat([{"text" => category.name, "id" => category.id, "leaf" => false,
10 "children" => get_tree(category.children,category)}])
11 end
12 else
13 data.concat([{"text" => category.name, "id" => category.id,
14 "cls" => "folder", "leaf" => false, "children" => []}])
15 end
16 }
17 return data
18 end
I added to textfield passwordfield textare submit tags new default attributes and style, so you can simply add borders, background and others, but also for textfield a new attribute:
1 def text_field_tag(name, value = nil, options = {})
2 options[:class] ||= "text_field"
3 if options[:onclick] == :clear_value
4 options.delete(:onclick)
5 options.merge!(:onblur => "if(this.value=='')this.value=this.defaultValue;",
So you can add some default text to your textfield:
1 <%= text_field :subscription, :email, :value => "Enter your e-mail", :onclick => :clear_value %>
I manage much pdf and I've no time for build them... so I convert my html in pdf with Prince Xml for example you can make a pdf with few lines with dry.
1 def print
2 @recipe = Recipe.find(params[:id])
3 if logged_in?
4 make_and_send_pdf("/site/recipes/print", "Italyabroad_Recipe_#{@recipe.id}.pdf")
5 else
6 redirect_to :controller => :base, :action => :login
7 end
8 end
Based on the beautifull plugin Jonathan Viney it's usefull for manage data tableless like contact form and output errors/warnings
Serializo is a very simple library for convert attributes of an serialization in method so we can use it in our form withou problem.
Example of use:
1 require 'digest/sha1'
2 class Account < ActiveRecord::Base
3 # Virtual attribute for the unencrypted password
4 attr_accessor :password, :data, :active
5
6 serialize :permissions
7
8 ...
9
10 def permissions
11 Serializo.generate(read_attribute(:permissions))
12 end
13
14 def permissions=(perm)
15 write_attribute(:permissions, perm.to_hash)
16 end
So in the view we can do:myaccount.permission.can_read = true
This is very usefull in form like:
1 <% fields_for "account[permissions]", @account.permissions do |perm| %>
2 <div><%= perm.text_field :dynamic_field, :style => "width:100%" %></div>
3 <% end %>
This is the roadmap that we want to follow in the next relases
Lipsiadmin is a new revolutionary admin for your projects.
Is developped by http://www.lipsiasoft.com that use it from 1 year in production enviroments.
Lipsiadmin is based on Ext Js 2.0. framework (with prototype adapter) and is ready for Rails 2.0.
This admin is for newbie developper but also for experts, is not entirely written in javascript because the aim of developper wose build in a agile way web/site apps so we use extjs in a new intelligent way a mixin of "old" html and new ajax functions, for example ext manage the layout of page, grids, tree and errors, but form are in html code.
Current revision is 2.0 updated at 26 July 2008
We daily put some howtos here
There is a list of screenshot here
We have some documentations of 3rd party plugins and our plugins here
Remeber to register and help us submitting bugs and request.
Rember for any question our forum
Contribute reporting bug or new fatures in our issues tracker
Contribute by adding fatures or patch. See this simple howto
Lipsiadmin is very simple to install use.
MacBook:rails DAddYE$ rails demoadmin
MacBook:rails DAddYE$ cd demoadmin/
MacBook:demoadmin DAddYE$script/plugin install git://github.com/Lipsiasoft/lipsiadmin.git
MacBook:demoadmin DAddYE$ script/generate lipsiadmin
MacBook:demoadmin DAddYE$ mate db/migrate/001_create_accounts.rb
Edit the migrations adding your first account data.
If you don't do that you can't get the email with the required activation code.
MacBook:demoadmin DAddYE$ mate config/config.yml
Remeber to edit also config.yml with your mail addres and your url of your site.
MacBook:demoadmin DAddYE$ rake db:create
MacBook:demoadmin DAddYE$ rake db:migrate
MacBook:demoadmin DAddYE$ script/server
Yes!! That's all your admin is ready to use!
Now for example we want to create a new model for our articles.
MacBook:demoadmin DAddYE$ script/generate model article
MacBook:demoadmin DAddYE$ mate db/migrate/004_create_articles.rb
Edit the new migration like that:
1 class CreateArticles < ActiveRecord::Migration
2 def self.up
3 create_table :articles do |t|
4 t.string :title, :tags
5 t.text :description, :description_short
6 t.timestamps
7 end
8 end
9
10 def self.down
11 drop_table :articles
12 end
13 end
MacBook:demoadmin DAddYE$ script/generate attachment article image
This will create
1 class AddAttachmentsImageToArtilce < ActiveRecord::Migration
2 def self.up
3 add_column :articles, :image_file_name, :string
4 add_column :articles, :image_content_type, :string
5 add_column :articles, :image_file_size, :integer
6 end
7
8 def self.down
9 remove_column :articles, :image_file_name
10 remove_column :articles, :image_content_type
11 remove_column :articles, :image_file_size
12 end
13 end
Now edit your model
1 class Article < ActiveRecord::Base
2 has_attached_file :image, :styles => { :normal => "780x360", :medium => "400x350", :thumb => "128x128!" }}
3 validates_attachment_presence :image
4 end
MacBook:demoadmin DAddYE$ rake db:migrate
Now we can generate the new admin pages with support for images.
MacBook:demoadmin DAddYE$ script/generate lipsiadmin_page article -i image
Okey ready? Stop and restart webrick! And go in your menu section and add some menus like the image bottom:
Login: info@lipsiasoft.com
Password: admin