Index by title

Editable Grid

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

ExtTree

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

HowTos

There you can find some short tutorial that show you how you can extend Lipsiadmin fatures

Customize Grids

Editable Tree


Plugins included in bundle

Paperclip

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?

Authenticated System

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.

Better Error Messages

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>

Better Error

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

Better Nested Set and Helper

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

Better Tag Helper

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 %>

Pdf Helper and Prince

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

ActiveRecord WithoutTable

Based on the beautifull plugin Jonathan Viney it's usefull for manage data tableless like contact form and output errors/warnings

Serializo

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 %>

Roadmap

This is the roadmap that we want to follow in the next relases

  1. Refactoring code (where necessary)
  2. Make better documentation (with the wiki and rdoc)
  3. Make better demo app that full show example usage of Lipsiadmin
  4. Make a new menu for make and edit Menu and Menu item (is what u do through migration)
  5. Make an Istant messenger through http://rubyforge.org/projects/ruburple/ that can be useful for customers for getting help from the developper
  6. Make Lipsiadmin ALSO totally desktop through AdobeAIR
  7. Make Lipsiadmin like OS ex: http://extjs.com/deploy/dev/examples/desktop/desktop.html

Lipsiadmin

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

Howtos

We daily put some howtos here

There is a list of screenshot here

Documentation of plugins

We have some documentations of 3rd party plugins and our plugins here

Remember

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

Overview

Installation

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!

How 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