Thursday, September 8, 2016

Make a Standard Flash Message Bar with Bootstrap Style in Rails Application

Normally, we have a very simple flash message such as “The data has been saved!” in the top of the browser to tell you the operation is done. It is just a simple text, sometimes we even don’t see it since it is not shining!

Fortunately, we can make a helper method accompany with Bootstrap classes to make it look better. It’s simple, useful and can be used as a standard code base for every project.

Here is how:

1. Add a line of ocde: <%= notice_message %> in

/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  :
  :
  <body>
    <div class="container" >
      <%= notice_message %>
      <%= yield %>
    </div>
  </body>
</html>

2. Add a helper method “notice_message” in

/app/helpers/application_helper.rb
module ApplicationHelper
  def notice_message
    alert_types = { notice: :success, alert: :danger }

    close_button_options = { class: "close", "data-dismiss" => "alert", "aria-hidden" => true }
    close_button = content_tag(:button, "×", close_button_options)

    alerts = flash.map do |type, message|
      alert_content = close_button + message

      alert_type = alert_types[type.to_sym] || type
      alert_class = "alert alert-#{alert_type} alert-dismissable"

      content_tag(:div, alert_content, class: alert_class)
    end

    alerts.join("\n").html_safe
  end
end
Once the helper is called, like:
redirect_to @size, notice: 'Size was successfully created.'
The flash notice shows:

Monday, September 5, 2016

Build an Order Form Using Google Spreadsheet Service and Google Apps Script

Google SpreadSheet is a free, convient and powerful tool to use for everybody. Not only the super easy usability but also programable capability allowed users to customize Google SpreadSheet to have more advanced functionalities. By using on-line editing and debugging tool, writting javascript and calling Google Spreadsheet Service seems to be an easy job to do.

In the Spreadsheet window, click the "Tool" and select "code editor".




There are four important classes needed to be noticed:
  • SpreadsheetApp
  • SpreadSheet
  • Range
  • Cell

Basic Operation

A user has to call these classes one by one in order to get the object to be controlled. For example:
// get active spreadsheet.
var ss = SpreadsheetApp.getActiveSpreadsheet(); 

// get spreadsheet named "Order Form". 
var sheet = ss.getSheetByName("Order Form");

// get range from first row to the max row, first column to the max column.
var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);

// get the first cell.
var aCell = seriesRange.getCell(1, 1);

Create Customized Menu












Google SpreadSheet allows us to cusmize menu. The demo code is:
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Order Form')
      .addItem('Clear From', 'clearForm')
      .addSeparator() 
      .addItem('Submit Order', 'cloneSheet')
      // .addSubMenu(ui.createMenu('Sub-menu')
      //     .addItem('Second item', 'menuItem2'))
      .addToUi();
  
  // Set the starting cursor location.
  setStartingPoint_();
}

How to refresh (clear) the spreadsheet?

function clearForm() {
  var ui = SpreadsheetApp.getUi()
  var response = ui.alert('Clear Form', 'Do you want to clear all data?', ui.ButtonSet.OK_CANCEL);
  if (response == ui.Button.CANCEL) {
    return;
  }
  
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  
  var sheet = ss.getSheetByName('Empty Order Form');
  sheet.copyTo(ss).setName('Clear Order Form');
  newSheet = ss.getSheetByName('Clear Order Form');
  ss.setActiveSheet(newSheet);
  ss.moveActiveSheet(0);

  var orderFormSheet = ss.getSheetByName('Order Form');
  ss.deleteSheet(orderFormSheet);

  newSheet.setName('Order Form');  
  
  setStartingPoint_();
}

function setStartingPoint_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  activeRange = sheet.getRange(5,2);
  sheet.setActiveRange(activeRange);
}

How to insert a row and do other logics?

function insertRow() {
  var startingRow = 4
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);
  var seriesValues = seriesRange.getValues();
  var seriesSelected = sheet.getRange((startingRow-1),1).getValue();   // .getRange("A3")
  
  // if series not selected, show warning dialog box 
  if (seriesSelected == "") {
    SpreadsheetApp.getUi().alert("Please select the series. Thank you.");
    return;
  };
  
  // find the row number of the title of series.
  // It also represents the sequence of the series, 
  // which can be used to determin the sequence of sheet.
  i = 0;
  while (seriesValues[i][0] != seriesSelected) {
    i++;
  };
  
  // Compensate the difference of starting point.
  seriesSelectedRow = i + startingRow;      
  
  // find the last row of the model.
  // Compensate the difference of starting point.
  var modelRow = seriesSelectedRow; 
  
  // "modelRow + 1" means start testing from the next row of the sub title. 
  while (!sheet.getRange(modelRow+1, 2).isBlank()) {
    modelRow += 1;
  };
  
  // if first model row is blank, then modelRaw = seriesSelectedRaw + 1
  if (modelRow == seriesSelectedRow) {
    modelRow += 1;
  };
  
  Logger.log("seriesSelectedRow: "+seriesSelectedRow)
  Logger.log("modelRow: "+modelRow)
    
  // This inserts one row after the last row of the model and focus on it.
  sheet.insertRowsAfter(modelRow,1);
  lastRowRange = sheet.getRange(modelRow+1,2);
  sheet.setActiveRange(lastRowRange);
  
  // Set the "TOTAL" formula
  sheet.getRange(modelRow+1, 7).setFormulaR1C1("=R[0]C[-2]*R[0]C[-1]")
}

Searching the right data

function onEdit() {
  var startingRow = 4
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // Quit if not at the "Order From" sheet.
  if (ss.getSheetName() != "Order Form") {
    return
  };
  
  var sheet = ss.getSheetByName("Order Form");  //getActiveSheet();
  var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);
  var ar = sheet.getActiveRange();
  var specSelected = ar.getValue();
  
  var row = ar.getRow();  // getA1Notation();
  Logger.log(row);
  
  // Quit if row at the heading part.
  if (row < startingRow) {
    return
  } 
  
  // find the series name where the avtive range belongs.
  
  var i = (row - 1);
  Logger.log(i);
 
  while (sheet.getRange(i,1).isBlank()) {
    i--;
  };
  
  // find the sequence of the series
  var seriesName = sheet.getRange(i,1).getValue()
  Logger.log(seriesName);
  
  var seriesSequence = 0;
  var j = 0;
  while (seriesRange.getValues()[j][0] != seriesName) {
    if (!seriesRange.getValues()[j][0] == '' ) {
      seriesSequence++;
    }; 
    j++;    
  };
  seriesSequence++;     // Cpmpensate the sequence.
  Logger.log(seriesSequence);
  
  // Get the worksheet containing the series data.
  var seriesSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[seriesSequence];
  var seriesSheetName = seriesSheet.getName();
  Logger.log(seriesSheet.getName());
  
  specRange = seriesSheet.getRange(1, 1, seriesSheet.getLastRow(), 4);
  specValues = specRange.getValues();
  Logger.log("seriesSheet:"+seriesSheet.getName());
  Logger.log(specValues);
 
  // find the price associated with the spec
  k = 0;
  while (specValues[k][0] != specSelected) {
    k++;
  };
  
  // Set price to order form
  priceSelected = specValues[k][3];
  sheet.getRange(row, 5).setValue(priceSelected);
  Logger.log(priceSelected);
}

How to find out the current cell where you are in edit?


function currentCell() {
  return SpreadsheetApp.getActiveRange().getA1Notation();
}

How to duplicate to a new spreadsheet?

// copy data from current SpreadSheet to a new SpreadSheet  
function cloneSheet() {
 
  // Show dialog box to ask if continue
  var ui = SpreadsheetApp.getUi();
  var response = ui.alert('Submit Order', 'Do you want to submit order?', ui.ButtonSet.OK_CANCEL);

  if (response == ui.Button.CANCEL) {
    return;
  }
  
  // source doc
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  
  // Get full range of data
  var SRange = sheet.getDataRange();
  // var SRange = sheet.getRange("A:G");  
 
  // get A1 notation identifying the range
  var A1Range = SRange.getA1Notation();
 
  // get the data values in range
  var SData = SRange.getValues();
  var SFormat = SRange.getNumberFormats();
  
  // target spreadsheet
  // var tss = SpreadsheetApp.openById(ssB);
  var ssNew = SpreadsheetApp.create('Mekk_2017_Order_'+ getDateTime_());
  
  // target sheet
  // var ts = tss.getSheetByName('Target Spreadsheet'); 
  var sheetNew = ssNew.getActiveSheet();
  
  // Clear the Google Sheet before copy
  // ts.clear({contentsOnly: true});
 
  // set the target range to the values of the source data
  sheetNew.getRange(A1Range).setValues(SData);
  sheetNew.getRange(A1Range).setNumberFormats(SFormat);
  
  // Sets the first column to a width which fits the text
  sheetNew.autoResizeColumn(1);
  sheetNew.autoResizeColumn(2);
  sheetNew.getRange('A3').clear();
  
  // Get the current folder
  var ssId = ss.getId();
  var parentFolders = DriveApp.getFileById(ssId).getParents();
  
  while (parentFolders.hasNext()) {
   var folder = parentFolders.next();  // parentFolders.next() is the current folder. 
  }; 
  
  // Get the id of new spreadsheet
  var ssNewFile = DriveApp.getFileById(ssNew.getId());
  var ssNewName = ssNewFile.getName();
  
  // Put the Spreadsheet to the current folder
  folder.addFile(ssNewFile);
  
  // Remove the Spreadsheet from the root folder
  DriveApp.getRootFolder().removeFile(ssNewFile);
  
  // user can also close the dialog by clicking the close button in its title bar.
  SpreadsheetApp.getUi().alert("The Order has been placed. Thank you.");  
}

How to get a proper datetime format?


function getDateTime_() {
 // This formats the date as Greenwich Mean Time in the format
 // year-month-dateThour-minute-second.
 
 var formattedDate = Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd'T'HH:mm:ss'Z'"); 
 
 return formattedDate;
}

Friday, August 26, 2016

Using Collection_Check_Boxes to Simplify User Interactions

Collection_check_boxes could be a very useful tool for simplify user interactions. Considering such scenario and table associations:

  • model
  • size
  • color
The model has size and color attributes. We hope to add and edit these three items on the same page in order to reduce complexity of operation.

First, the three models must have associations with each other.

model:
has_and_belongs_to_many :sizes
has_and_belongs_to_many :colors
size:
has_and_belongs_to_many :models
color:
has_and_belongs_to_many :models

Secondly, add collection_check_boxes. 

view/models/_form.html.erb
for “size” attribute
<%= f.collection_check_boxes :size_ids, Size.all, :id, :name do |b| %>
  <div class="collection-check-box">
    <%= b.check_box %>
    <%= b.label %>
    </div>
  <% end %>
for “color” attribute
<%= f.collection_check_boxes :color_ids, Color.all, :id, :name do |b| %>
  <div class="collection-check-box">
    <%= b.check_box %>
    <%= b.label %>
    </div>
  <% end %>
Thus, the “size” and “color” attributes can be added and edited in the same page along with “model”. It simplifies the user operation and make the app more intuitive.

Friday, August 19, 2016

Using jQuery Ajax in Rails Appliccations

Normally, we build a restful rails application when a ordering transaction system is needed. However, imaging a scenario such as a bicycle with three attributes:
  • model
  • size
  • color
The size and color options vary by model and they are not always the same combination across all models. For example, model A has size options of 700c X 48cm, 700c X 50cm and 700c X 52cm and color options of red, blue and black. Model B may have 700c X 48cm and 700c X 50cm and color options of green and orange. Therefore, when model changes during user operation, the size and color options must be changed accordingly.
That is a big design problem for a Rails application. Unless there is a a button for user to press and refresh the page while a model has been chosen, the Rails app has no idea when to refresh the page.
I use jQuery to solve this problem.
jQuery ajax can be used to listen the change of the model selection. When the model change happens, ajax calls the get_info function in orderdetails controller.
/view/orderdetails/new.html.erb
<script type="text/javascript">
$('#orderdetail_model_id').on('change', function() {
    $.ajax({
      url: '/get_info',
      type: 'GET',
      data: { model_id: this.value }
    })
  });
</script>  
The path is set in the routes.rb first.
get '/get_info', to: 'orderdetails#get_info', as: :get_info
Then ‘get_info’ function returns the instance variables needed for the page refresh.
controller orderdetails#get_info
def get_info
  @model  = Model.find(params[:model_id])
  @sizes  = @model.sizes
  @colors = @model.colors
  respond_to do |format|
    format.js
  end
end
Here is the magic: ‘get_info.js.erb’ replace the size and color select in their divs . The size and color options changes while model changes.
get_info.js.erb
$("#orderdetail_select_size_id").html("<%= escape_javascript(select("orderdetail", "size_id", @sizes.collect {|s| [s.name, s.id ] }, { include_blank: false })) %>");
$("#orderdetail_select_color_id").html("<%= escape_javascript(select("orderdetail", "color_id", @colors.collect {|c| [c.name, c.id ] }, { include_blank: false })) %>");
$("#orderdetail_text_field_price").html("<%= escape_javascript(text_field_tag("orderdetail", @model.price, style: "text-align: right", disabled: true )) %>");
TIP:
The code to be replaced need to be modified in the ‘get_info.js.erb’.
The ‘f.select’ causes error because ‘f’ is not defined in the replaced code portion.
f.select("size_id", Model.find_by_id(orderdetail.model_id).sizes.collect {|s| [ s.name, s.id ] }, { include_blank: false })
We have to replace it to the name of the select. By inspecting the generated html file, the name of the select is “orderdetail_select_size_id”. Using select_tag and name the variable as “orderdetail”, the problem solved and it works.
select("orderdetail", "size_id", @sizes.collect {|s| [s.name, s.id ] }, { include_blank: false }))"

Thursday, March 24, 2016

Rails 商務網站 x 即戰力班 心得


對網站建置有興趣的人總是聽說,用Rails架站很快,Ruby 語法人性化又易讀,功能強大,擁有龐大生態系,不管什麼應用,總是找得到相對應的gem。是適合新人至高手都可以使用的成熟技術。

事實上是這樣嗎?是的,是這樣的。不過,前提是,要有人帶。不是不能自己摸索自學,而是,Liffe is short,你我有更多想要追求的目標,時間往往不夠用。除非你是程式潔癖愛好者,非自己摸索出來的不算數。

是的,Life is short。看著那些大神級的人物飛快的demo影片,恨不得自己也跟他們一樣,能夠三把兩下就把網站搞定,做出又酷又炫的效果,讓眾人朝拜一番。總想自己也能夠像武俠小說裡的主角,某天遇到一位深藏不露的高手,就把畢生絕學傳授給你,打通任督二脈,瞬間獲得超過一甲子功力,從此能夠華山論劍,笑傲江湖啊~~。

是的,「Rails 商務網站 x 即戰力班」就是這樣子橫空出世了!

相關的資訊,例如學徒制、串接金流、線上助教解答等等特色,網站上都查的到,我要說的是查不到的東西。

我的經驗。

我是一位 IT 及 ERP資深導入顧問。也是一位專業攝影師、影像創作者(攝影集、攝影展)。我的豐富人生經驗告訴我,要成為業界的頂尖人物,除了努力學習之外,還要「有效率的學習」。什麼是「有效率的學習」?就是要把知識經驗盡量以下面的形式學習消化之:

  • 去蕪存菁的理論基礎
  • 別人寶貴的實戰經驗
  • 因地制宜的權衡變通
  • 自我恆常不斷的練習

我之所以會超過一項的專業技能,除了是興趣之外,有效率的學習是很重要的因素。所以學習 Rails,我也是用這種方式在自我學習著。而「Rails 商務網站 x 即戰力班」剛剛好符合我「有效率學習」的原則。


一,去蕪存菁的理論基礎

雖然理論很重要,它能讓學者見樹又見林,知道在這個體系中的來龍去脈。但是在實際應用上,其實不需要太過於學術性的東西,太沉浸其中,反招葛藤。所以,理論以實用為主,用以見其大架構為主,不拘泥於小節。

這個班的教學,在有需要的地方才會提到相對應的理論背景,而不是像教科書般宣說一堆術語與抽象名詞。

二,別人寶貴的實戰經驗

實戰經驗是無價的,參考別人走過的路,自己往往可以省下龐大的摸索時間與資源。

Xdite 在 Rails 的豐富經驗就不用多說,跟著走絕對是一條捷徑。

三,因地制宜的權衡變通

固然學習別人的經驗是非常重要,但是總歸是別人的。自己的環境與時間等條件不同,一定要懂得因地制宜。

像我之前也上過在美國開班,遠端上課的 Rails 課程。在金流方面用的是Stripe,但在台灣便用不上。此班用的是智付寶,完全符合台灣的環境。在金流串接這塊完全是本地化。


四,自我恆常不斷的練習

不斷的練習是避免生疏的不二法門,也是得到該學門更深層體悟的最佳方法。我在攝影這塊,一直奉行不渝,只要有空,就會帶著相機出門練習。

而此班亦然,非常強調實作。Xdite 強調,一定要把作業做三遍以上,做到不需要看解答為止。我覺得這一點,其實是最最重要的。很多時候,在一片通紅的錯誤中,會看到自己的盲點;  更多時候,在第n遍的練習中,忽然體悟到,「啊!原來如此」,瞬間了解 Rails 的設計邏輯與慣例。

(不過,做三遍以上的壓力很大,我那幾週除了上班、吃飯、睡覺之外,其他時間都在做練習。也因壓力過大,造成胃痛好幾天。)

--------------------

說到這裡,您不覺得從此功力大增,新手者極速入門,老鳥者,溫故知新,前後貫通,更上一層樓。

但是.........

並不是從此海闊天空。

因為,這個班的作法,是在極短時間內,壓縮大量 Rails 的知識及 Xdite 個人經驗,密集的傳授給學員。所以,想要笑傲江湖,還要達成以下目標:

新手者:

1. 練習、練習、再練習。
2. 千萬要補足基礎的知識:如 Ruby 語言的基礎、OOP的基礎、database 的基礎、html、http、javascript、jQuery 等等的網頁基礎。
3. 自己的 bug 自己解。雖然此班的助教解救深陷bug的學員功力一流,但我深深覺得,要想徹底了解 Rails ,一定要自己來解bug而不靠別人。這樣學習來的經驗才是自己的。

老鳥者:

1. 包含新手的注意事項
2. 作業的延伸。如:自己再加功能、更多 user story。
3. 自己自創專案。自己做想要做的網站。
4. 行有餘力,了解各種行業的商業邏輯。像ERP的理論及架構,是了解企業內部運作的最佳範例。寫商務網站,不可能不觸及企業運作。

----------------------

寫到這裡,應該要結束了。

如果你認為,以上的文字很多都是 sales talk,我承認。

但是,人生是自己的。記住:Life is short!