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 }))"