CSC/ECE 517 Spring 2015 E1526: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
Line 280: Line 280:
Once logged in, choosing Manage/users option from the navbar, gets us routed to /users page which is defined in list.html.erb file. This file can be broken down to 5 constituents:
Once logged in, choosing Manage/users option from the navbar, gets us routed to /users page which is defined in list.html.erb file. This file can be broken down to 5 constituents:


1) Fetching Users
1) Fetching Users <br>
2) Paginating Users
2) Paginating Users <br>
3) Searching Users
3) Searching Users <br>
4) Showing Users
4) Showing Users <br>
5) Editing and Deleting Users
5) Editing and Deleting Users <br>
 


===Code Details===
===Code Details===

Revision as of 18:39, 3 May 2015

E1526. Responsive web design for Expertiza

Strategy

We analyzed the three Expertiza pages that we need to modify for this project, as well as all of their dependencies, and tried to identify all the tasks that we need to complete for this project. It became clear to us that a large majority of this project will be based on AngularJS, while the small fraction will be related to Bootstrap. Therefore, our strategy was to jumpstart this project by having all 4 team members work together on adding AngularJS to the Login page. Once the Login page was completed, we split in two groups. One group worked on implementing AngularJS on Manage-course page, while the other group worked on introducing AngularJS to Manage-User page. The reasoning behind this strategy was that all of us working together on the Login page first allowed us to obtain some basic understanding of AngularJS and feel more comfortable using this impressively useful and powerful framework. We completed AngularJS constituents first by connecting some of the Rails controllers to AngularJS controllers and then, as a final step, we add Bootstrap. Our decision was based on the fact that Bootstrap does not require much work to be completed and it can be considered to be a pretty straight-forward element of the project.

Guide for future students

1. Learn how to integrate AngularJS into Rails, using Rails original routing system.

2. Learn AngularJS and Coffeescript, since the app.js is written in Coffeescript now.

3. Learn Bootstrap, including the grid system, responsive design and etc.

4. Read the code written by us, to know

   a. how to pass data from Rails controller and view to AngularJS; 
   b. how to pass data back to Rails view from AngularJS; 
   c. how to convert JSON formatted data using in AngularJS to Rails hash and vice versa.
   d. how to present the data to Rails view

Integration

This section represent the cornerstone of this project. It consciously explains how Angular and Rails can be integrated into a powerful system capable of providing solid control over both the backend and frontend development of your application.

Steps to interconnect Rails and AngularJS

These guidelines assume that you have a Rails project in place and that you are intending to add AngularJS to it now. Please follow the steps bellow:

1. In the Gemfile, ADD gem ‘bower-rails’ AND gem ‘angular-rails-templates’, then REMOVE ‘gem turbolinks'

2. run

 bundle install

3. Create a Bowerfile. It is used by AngularJS and it can be thought of as the Gemfile that Rails uses.

4. In the Bowerfile, ADD asset ‘angular’ , asset ‘bootstrap-sass-official’, asset ‘angular-route’ AND asset ‘angular-boostrap’(for ui.bootstrap injection)

5. run

 rake bower:install 

. Bower installs dependencies in vender/assets/bower_components.

6. ADD the following content to the config/application.rb

 config.assets.paths << Rails.root.join("vendor","assets","bower_components")
 
config.assets.paths << Rails.root.join("vendor","assets","bower_components","bootstrap-sass-official","assets","fonts")

config.assets.precompile << %r(.*.(?:eot|svg|ttf|woff|woff2)$) 

7. REMOVE //= require turbolinks AND ADD

//= require angular/angular
//= require angular-route/angular-route
//= require angular-rails-templates
//= require angular-bootstrap 

to assets/javascripts/application.js

8. Rename assets/stylesheets/application.css to assets/stylesheets/application.css.scss.

9. ADD

@import "bootstrap-sass-official/assets/stylesheets/bootstrap-sprockets"; and
@import "bootstrap-sass-official/assets/stylesheets/bootstrap"; 

to application.css.scss

Since we are did not get to build a new website using AngularJS from scratch, it was a good idea to retain the routing system of Rails rather than implement it in AngularJS. So, we added a new tag div ng-app='MPApp' into the app/views/layout/application.html.erb to include all pages into the scope of our AngularJS app, named ‘MPApp’. Then, for the home page app/views/content_pages/view.html.erb, we added a tag to make this page controlled by ‘testCtrl’, which was defined in app.coffee.

AngularJS's point of view

1. Retrieving data from database Because it is not appropriate to let AngularJS to directly fetch data from the MySQL database, the only way we could load data was through a Rails controller. For example, it is possible to do a http get/post request(GET /users/fetch_10_users) from AngularJS controller using $http injection or jQuery AJAX, which calls a new method ‘def fetch_10_users’ in the users_controller, which execute some database query via accessing methods in the user model, and returns json formatted data to the AngularJS controller. After getting the returning data, the AngularJS controller can present the data to the screen.

2. Build custom directives Instead of using the original Rails page, we decided to replace some forms and components using AngularJS directives, which enabled us to do some live modifications to the pages without loading.

Rails' point of view

The initial problem was how to provide entries from the database to AngularJS. We decided to create methods in Rails controller which will listen to the incoming AngulaJS and use a select statement to fetch a couple of entries from the database. These entries are then stored in a hash, converted to json and passed to AngularJS controller. AngularJS will then send another query request to Rails for the next set of entries. This strategy greatly reduces the loading time, making the time needed for representing the page kept at the minimum.

The specifics

This section explains the very detailed actions and their results related to the 3 specific pages that were used to integrate AngularJS. Numerous snapshots are provided with the goal to make our approach very clear.

Login Page and Navigation Bar

Login procedure: ‘Login’ button clicked → auth_controller#login → auth_controller#after_login(user) → if user.role != ‘student’ → tree_display#drill → tree_display#list
If session[:menu] is defined, that is, when a user is logging in, the page will render the content of variable session[:menu].

<ul class="nav navbar-nav">
    <% if session[:menu] %>
       <%= render :partial => 'menu_items/suckerfish',
          :locals => { items: session[:menu].get_menu(0), level: 0 } %>
     <% end %>
</ul>

<ul class="nav navbar-nav">
   <% if session[:menu] %>
      <%= render :partial => 'menu_items/suckerfish',
         :locals => { items: session[:menu].get_menu(0), level: 0 } %>
  <% end %>
 </ul>

For each menu item in the nav bar, it is a dropdown menu now. For different level of dropdown menu, we apply different CSS style to it.

<% level += 1 %>
<% items.each do |item_id|
  item = session[:menu].get_item(item_id)
  selected = session[:menu].selected?(item.id)
  long_name = item.name.split(/\W/).collect{|word| word.capitalize}.join(' ')
%>
<% if level == 1 %>
<li class="dropdown first-level">
<% elsif level == 2 %>
<li class="dropdown-submenu">
<% else %>
<li class="dropdown">
<% end %>
  <a href=<%= URI.encode("/menu/#{item.name}") %>
  <%= if item.children
        ' class="daddy"'
      end %>title='<%= long_name %>'><%= item.label.html_safe %></a>
    <% if item.children -%>
    <% if level == 1 %>
    <ul class="dropdown-menu multi-level">
      <%= render :partial => 'menu_items/suckerfish', :locals => {:items => item.children, :level => level} %>
    </ul>
    <% else %>
    <ul class="dropdown-menu">
        <%= render :partial => 'menu_items/suckerfish', :locals => {:items => item.children, :level => level} %>
      </ul>
    <% end %>
    <% end -%>
</li>
<% end -%>

Please also refer to app/assets/application.css.scss for further details.

Manage-course page

Design

After logging in, only the table frame is loaded into the page by rails. Then AngularJS takes over in the background and populate the content into the table. After all first-level content is populated into the table, in this case, all courses info belongs to the first level and their assignment is the second-level content.

After all first level content is loaded into the table, the Angular controller starts to initialize POST request for second level content.

In all, rendering the tree_display page is divided into 3 steps:
1. render the table frame,
2. render the first level table content.
3. fetch deeper level data.

In the past, these 3 steps are done before rendering the page, which takes about 10-20 seconds. But now, it takes only 2-3 seconds to finish the first 2 steps, and 2 more seconds for the 3rd step.

Moreover, the sorting and table record filtering is integrated into the table. No redirecting or re-rendering is needed for sorting or filtering, which was needed in the past.

Code details

Pass Rails controller params to view in tree_display_controller#list

angularParams = {}
angularParams[:search] = @search
angularParams[:show] = @show
angularParams[:child_nodes] = @child_nodes
angularParams[:sortvar] = @sortvar
angularParams[:sortorder] = @sortorder
angularParams[:user_id] = session[:user].id
angularParams[:nodeType] = 'FolderNode'
@angularParamsJSON = angularParams.to_json

Then the view passes to AngularJS

<div ng-controller='TreeCtrl'>
<div ng-init="init('<%= @angularParamsJSON %>')"></div>

AngularJS handles it, after all basic elements are loaded onto the page. Then AngularJS initializes a POST request for retrieving more data to populate the table.

$scope.init = (value) ->
    $scope.angularParams = JSON.parse(value)
    $http.post('/tree_display/get_children_node_ng', {
      "angularParams": $scope.angularParams
      })
    .success((data) ->
      $scope.tableContent = {}
      $scope.displayTableContent = {}
      $scope.lastLoadNum = {}
      $scope.typeList = []
      console.log data
      for nodeType, outerNode of data
        $scope.tableContent[nodeType] = []
        $scope.lastLoadNum[nodeType] = 0
        $scope.displayTableContent[nodeType] = []
        $scope.typeList.push nodeType
        # outerNode is the Assignments/Courses/Questionnaires
        for node in outerNode
          $scope.tableContent[nodeType].push node
          $scope.fetchCellContent(nodeType, node)
        $scope.getMoreContent(nodeType, 1)
      )

Fetch table cell content recursively and put them into a hash

$scope.fetchCellContent = (nodeType, node) ->
    $scope.newParams = {}
    $scope.newParams["sortvar"] = $scope.angularParams["sortvar"]
    $scope.newParams["sortorder"] = $scope.angularParams["sortorder"]
    $scope.newParams["search"] = $scope.angularParams["search"]
    $scope.newParams["show"] = $scope.angularParams["show"]
    $scope.newParams["user_id"] = $scope.angularParams["user_id"]
    key = node.name + "|" + node.directory
    $scope.newParams["key"] = key
    # console.log key
    if nodeType == 'Assignments'
      $scope.newParams["nodeType"] = 'AssignmentNode'
    else if nodeType == 'Courses'
      $scope.newParams["nodeType"] = 'CourseNode'
    else if nodeType == 'Questionnaires'
      $scope.newParams["nodeType"] = 'FolderNode'
    else
      $scope.newParams["nodeType"] = nodeType

    $scope.newParams["child_nodes"] = node.nodeinfo
    $http.post('/tree_display/get_children_node_2_ng', {
      "angularParams": $scope.newParams
      })
    .success((data) ->
      if data.length > 0
        for newNode in data
          if not $scope.allData[newNode.key]
            $scope.allData[newNode.key] = []
          $scope.display[newNode.key] = false
          $scope.allData[newNode.key].push newNode
          $scope.fetchCellContent(newNode.type, newNode)

      )

After all data is fetched, the Rails view is able to presenting them onto the page!

<tbody ng-repeat="record in displayTableContent['Courses'] | filter:searchText | orderBy:predicate:reverse">
          <tr ng-click="showCellContent(record.name, record.directory)" class="clickable-row">
            <td>{{record.name}}</td>
            <td>{{record.directory}}</td>
            <td>{{record.creation_date}}</td>
            <td>{{record.updated_date}}</td>
            <td>
              
            </td>

          </tr>
          <tr class="no-cursor-tr no-color-tr" ng-if="display[record.name+'|'+record.directory] && allData[record.name+'|'+record.directory]">
          <td colspan="3">
            <table class="normal-table table table-bordered table-striped compact-table" ng-repeat="cell in cellContent">
              <tbody>
                <tr class="no-cursor-tr no-color-tr">
                  <td><b>Name</b>: {{cell.name}}</td>
                </tr>
                <tr class="no-cursor-tr no-color-tr">
                  <td><b>Directory</b>: {{cell.directory}}</td>
                </tr>
                <tr class="no-cursor-tr no-color-tr">
                  <td><b>Creation Date</b>: {{cell.creation_date}}</td>
                </tr>
                <tr class="no-cursor-tr no-color-tr">
                  <td><b>Modified Date</b>: {{cell.updated_date}}</td>
                </tr>
              </tbody>
            </table>
          </td>
          </tr>
        </tbody>

Manage Users page

Design

Once logged in, choosing Manage/users option from the navbar, gets us routed to /users page which is defined in list.html.erb file. This file can be broken down to 5 constituents:

1) Fetching Users
2) Paginating Users
3) Searching Users
4) Showing Users
5) Editing and Deleting Users

Code Details

1) Fetching Users Fetching users was challenging at first due to an extremely large number of user entries (total of 5300). Fetch of such large data produced a very long delay that was one of the major UX problems in legacy version of Expertiza. This problem has been solved with Angular by sending a post request that gets 300 users and then another post request which gets all the users. This approach provides a quick response and the first 300 users can be seen right away in the form of 3 pages (default set to 100 users per page). While the user is looking at the first couple of entries, the rest of the entries are fetched in the background. This allows for a fast response, full functionality as well as complete data.

In app/assets/javascripts/app.coffee:

$scope.getUsers = (fn) ->

    $http.post('/users/get_users_ng', {
      'fetchNumber': fn
    })
    .success((receivedUsers) ->
      console.log receivedUsers
      for user in receivedUsers
        $scope.users.push user
        $scope.user_display[user.object.id] = false
        $scope.editProfileVisible[user.object.id] = false

      $scope.fetchNumber+=1
      console.log $scope.users.length
      console.log $scope.listSize

      if $scope.users.length < $scope.listSize
        $scope.getUsers($scope.fetchNumber)
      )

  $scope.getUserListSize = () ->
    $http.get('/users/get_users_list_ng')
    .success((listSize) ->
      $scope.listSize = listSize
      )

This process is initiated by init() command shown bellow:

$scope.init = (value) ->
    $scope.user_display = {}
    $scope.editProfileVisible = {}
    if $scope.users.length == $scope.listSize
      return
    else if $scope.users.length == 0
      $scope.listSize = 0
      $scope.getUserListSize()
      $scope.fetchNumber = 0
    $scope.getUsers(($scope.fetchNumber))
    $scope.currentPage = 0
    $scope.pagination(50)

2) Paginating Users


3) Searching Users 4) Showing Users Clicking on each user name in the table of users uncovers a template that shows the information about that specific user. This is accomplished in a very simple way by setting the profileVisible flag to true. The information about that specific user is extracted from displayedUser object provided by showUser() method from UsersPageCtrl controller in app.coffee file.

In views/users/list.html.erb:

<div ng-if="profileVisible == true">
  <h1><b>User: {{displayedUser.object.name}}</b></h1>
  <br>
  <h3><b>Full Name: </b>{{displayedUser.object.fullname}}</h3>
  <h3><b>Email Address: </b>{{displayedUser.object.email}}</h3>
  <h3><b>Role: </b>{{displayedUser.role}}</h3>
  <br>

  <div>
    <button ng-click="showTable(true)" type="button" class="btn btn-primary">Back</button>
    <button ng-click="editUser()" type="button" class="btn btn-success">Edit</button>
    <button ng-click="deleteUser()" type="button" class="btn btn-danger">Delete</button>
  </div>
</div>

In app/assets/javascripts/app.coffee:

  $scope.showUser = (user_id) ->
    $scope.user_display[user_id] = !$scope.user_display[user_id]
    $scope.editProfileVisible[user_id] = false

5) Editing and Deleting Users

Editing Users is implemented in a similar way as showing users. The template contains ng-if statement which allows us to display edit page for each user. editUser() method from UsersPageCtrl controller specified in app.coffee displays information of the displayedUser but only after the Save button is clicked it saves the information from displayedUsers into updatedUser variable. POST request with :id parameter is then sent to update method in users_controller of Rails, which then updates this user information in the database.

  $scope.editUser = (user) ->
    # $scope.showUser(false)
    $scope.editProfileVisible[user.object.id] = !$scope.editProfileVisible[user.object.id]
    $scope.updatedUser = user