CSC/ECE 517 Spring 2015 E1526: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
 
(33 intermediate revisions by 3 users not shown)
Line 1: Line 1:
<font size="5"><b>E1526. Responsive web design for Expertiza</b></font><br><br><br>
<font size="5"><b>E1526. Responsive web design for Expertiza</b></font><br><br>


__TOC__
__TOC__


= Problem Statement =
= Strategy =


Despite an amazing set of functionality Expertiza offers, there are numerous parts of it that could use a more stylish look and an improved user experience. The goal of this project is to use both Bootstrap and AngularJS to improve the look of the entire Expertiza including the representations of buttons, tables, and other elements. Certain changes in design will also improve the efficiency of the web app when it comes to the amount of time it takes for a page to be loaded.
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.


= List of Tasks =
= Guide for future students =
Below are detailed explanations to the tasks listed in the description documentation.
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.


== 1. Replace Buttons and Links to Bootstrap styled Buttons ==
3. Learn Bootstrap, including the grid system, responsive design and etc.
Expertiza, being an extensive web application with numerous features, heavily relies on buttons when it comes to interacting with the user. We plan to improve the user experience by replacing the plain html buttons with stylish bootstrap buttons. However, numerous elements in the current version of Expertiza, like 'Back' element, are hyperlinks rather than buttons. To make the design of Expertiza consistent, our goal is to replace this plain-styled text with color coded Bootstrap buttons.


== 2. Create new theme ==
4. Read the code written by us, to know
One of the entities responsible for the primitive look of Expertiza, as of now, is the lack of fixed navbar. The new design of Expertiza will include the fixed menu bar on the top of the page. Please refer to the screenshot shown below for an example from Virgin American website. As it can be seen on the screenshot, the user has scrolled down the page, but the menu bar is still visible on the top of the page, making it convenient for the user to navigate throughout the website.


[[File:Picture4-2.png]]
    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


== 3. Auto hides contents on the dashboard for instructors ==
= Integration =
Please refer to the later part of this article: [[#Manage-Course page|Manage-Course page]] for viewing the current design
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.


Now, when trying to open manage-courses, all records are loaded before rendering them onto the page, which takes too long time to load and is not responsive at all.
== Steps to interconnect Rails and AngularJS ==


Better practice is on Virgin American’s website: https://www.virginamerica.com/book/rt/a1/sfo_bos/20150401_20150402
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:


When the website is representing the calendar, the price for each day is not actually loaded. But the skeleton of the page, which is the calendar in this case, can be displayed first.
1. In the Gemfile, ADD gem ‘bower-rails’ AND gem ‘angular-rails-templates’, then REMOVE ‘gem turbolinks'


[[File:Picture5.png]]
2. run <pre> bundle install</pre>


So in Expertiza, we can create several buttons for course semesters in the page. After clicking on Fall 2014, the courses for Fall 2014 will come out.  
3. Create a Bowerfile. It is used by AngularJS and it can be thought of as the Gemfile that Rails uses.


And if the list is still too long and takes a long time load, we can use a ‘show more’ button ,or automatically load more data when the scroll bar reaching the end of the page, to minimize the content we need to load after one single mouse click.  
4. In the Bowerfile, ADD asset ‘angular’ , asset ‘bootstrap-sass-official’, asset ‘angular-route’ AND asset ‘angular-boostrap’(for ui.bootstrap injection)


[[File:Picture6.png]]
5. run <pre> rake bower:install </pre>. Bower installs dependencies in vender/assets/bower_components.


6.  ADD the following content to the config/application.rb
<pre>
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)$)


== 4. Simplify Action Panel ==
</pre>
We will need to reduce the number of buttons. For example, the first row are actions for assignments and the second row is for participant. So we can replace them to 5 buttons with responsive design, that is, no redirecting happens after clicking on the buttons.


[[File:Picture7.png]]
7. REMOVE //= require turbolinks AND ADD
<pre>
//= require angular/angular
//= require angular-route/angular-route
//= require angular-rails-templates
//= require angular-bootstrap
</pre>
to assets/javascripts/application.js


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


9. ADD
<pre>
@import "bootstrap-sass-official/assets/stylesheets/bootstrap-sprockets"; and
@import "bootstrap-sass-official/assets/stylesheets/bootstrap";
</pre>
to application.css.scss


== 5. Offer Better UI for Managing Users ==
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 <code> div ng-app='MPApp'</code> 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.


Please refer to the later part of this article: [[#Manage-User page|Manage-User page]]
== 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.


== 6. Make grades_view Responsive with Better Design ==
== 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.


Similar to the previous tasks, we will need to make it responsive.  
=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.


After clicking on ‘Your scores’, all reviews are loaded before rendering the page now. That takes a long time to load and the length of review list is too long.
== 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<br/>
If session[:menu] is defined, that is, when a user is logging in, the page will render the content of variable session[:menu].
<pre>
<ul class="nav navbar-nav">
    <% if session[:menu] %>
      <%= render :partial => 'menu_items/suckerfish',
          :locals => { items: session[:menu].get_menu(0), level: 0 } %>
    <% end %>
</ul>


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


=Design Patterns=
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.
<pre>
<% 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 -%>
</pre>
Please also refer to app/assets/application.css.scss for further details.


As an assignment with a goal of improving the graphic design and responsiveness of Expertiza web application, this project mainly follows design patterns from two design pattern groups: structural and behavioral.
== 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.


==Structural Design Patterns==
After all first level content is loaded into the table, the Angular controller starts to initialize POST request for second level content.
Flyweight, a design pattern aiming to minimize the memory usage by sharing as much data as possible, is heavily implemented on the css and bootstrap side. Our team leans towards creating classes for styling that can be efficiently reused in a variety of Expertiza sections, rather than separately refining the design of each little section. This will both save us time and keep the code concise, while optimizing the memory needed to store the code.


Another structural design pattern that will be seen in this project is Front Controller pattern. While this pattern suggests that there is a single controller that takes all the requests, when we have both Rails and AngularJS coexisting, we can think of Rails framework as being the "single bridge" to the database from AngularJS's stand point. AngularJS, being an outstanding front-end framework, will interact with the user and pass all the request to Rails framework. Rails framework then queries/updates the database and provides AngularJS with the data.
In all, rendering the tree_display page is divided into 3 steps: <br>
1. render the table frame, <br>
2. render the first level table content. <br>
3. fetch deeper level data. <br>


Finally, the third structural design pattern that this project follows is Module pattern. Modules are one of the essential constructs in the skeleton of AngularJS framework and, hence, this project is bound to follow module pattern.
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.
[[File:E1526-1.png]]


==Behavioral Design Patterns==
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.
Since the large part of this project is to iterate through the database query and show the data to the user with a reduced delay, this project follows the well known Iterator pattern.


Along the similar lines, Observer pattern is followed to efficiently handle query requests to a large database (the main culprit behind the unpleasantly long delay). Our project intends to tackle this challenge by fetching only a screen-full of results and showing it to the user right away (fast response). While the user is looking at this first chunk of data displayed on the screen, further queries are made to the database in the background and the rest of the matching results are being returned, efficiently populating the page further. This approach greatly improves the user experience since it does not leave the user waiting empty-handed until Rails framework completes the full query of the database. <br>
=== Code details ===
[[File:E1526-2.png]]
Pass Rails controller params to view in tree_display_controller#list
<pre>
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
</pre>


= Pages To Be Modified =
Then the view passes to AngularJS
For this project, it is difficult to re-design all webpages and make them responsive. After discussing with the contact person, we will be focusing only on these 3 pages:
<pre>
<div ng-controller='TreeCtrl'>
<div ng-init="init('<%= @angularParamsJSON %>')"></div>
</pre>


=='''Login Page'''==
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.
[[File:Picture1.png]]
<pre>
$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)
      )
</pre>


=== What's wrong with it ===
Fetch table cell content recursively and put them into a hash
1. Now it takes more than 15 seconds to even login to the admin's home page with the sample development database. This is definitely not good enough for a daily used web application.  
<pre>
$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


The cause for the long rendering time is that it will be redirected to 'tree_display/list', which needs to fetch all questionnaires, courses and assignments before page rendering.
    $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)


2. The webpage looks primitive.
      )
</pre>


=== What changes need to be made ===
After all data is fetched, the Rails view is able to presenting them onto the page!
1. Actually it is unnecessary to fetch all data at the very beginning; if there is too much data to present, the list will be extremely long and it is quite hard for users to locate a specific course.  
<pre>
<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>


So, we will use AngularJS and jQuery to make a asynchronous webpage that delays the database query until all the basic html elements are correctly rendered, or until the user explicitly asks for that part of data.
          </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>
</pre>


2. Bootstrap will be applied for better user interfaces.
== 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:


=='''Manage-Course page'''==
1) Fetching Users <br>
[[File:Picture2.png]]
2) Paginating Users <br>
3) Searching Users <br>
4) Showing Users <br>
5) Editing and Deleting Users <br>


=== What's wrong with it ===
===Code Details===
As illustrated above, now the displaying list is too long and it takes too long to scroll to the end, which makes locating a specific course quite difficult.


Also, loading time can be reduced.
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.


=== What changes need to be made ===
In app/assets/javascripts/app.coffee:
1. Create several buttons for different time intervals, for example, after clicking on the button '2013-2014', only courses for 2013 to 2014 are presenting, and there is no redirection during this process.


2. The database should not be executed before the user clicks on the button. Only parts of databased is fetched when each button is clicked.
<pre>
$scope.getUsers = (fn) ->


3. Bootstrap will be applied for better user interfaces.
    $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)
      )


=='''Manage-User page'''==
  $scope.getUserListSize = () ->
[[File:Picture3.png]]
    $http.get('/users/get_users_list_ng')
    .success((listSize) ->
      $scope.listSize = listSize
      )
</pre>


=== What's wrong with it ===
This process is initiated by init() command shown bellow:
1. The list is too long: if there are 20k users in the database, there will be 20k rows in this table in a one page!


2. When clicking on the letter A-Z, redirections happen and the whole page is reloaded.
<pre>
$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)
</pre>


=== What changes need to be made ===
The view resulting from this code structure can be seen bellow:
1. Load only a given number of records to the view, such as 100 records, when accessing into this page for the first time.


2. Using AngularJS to eliminate the redirections and page reloading for better UI performance.
[[File:list_users.png]]
 
 
2) Paginating Users
 
[[File:Pag_src.png]]
 
In order to paginate users, a filter needs to be set in order to limit the number of users being shown:
 
In views/users/list.html.erb:
<pre>
<tbody ng-repeat="user in users | filter:search | startFrom:currentPage*pageSize | limitTo:pageSize">
        <tr ng-click="showUser(user.object.id)">
          <td>{{user.object.name}}</td>
          <td>{{user.object.fullname}}</td>
          <td>{{user.object.email}}</td>
          <td>{{user.role}}</td>
          <td>{{user.parent}}</td>
          <td>{{user.email_on_review}}</td>
          <td>{{user.email_on_submission}}</td>
          <td>{{user.email_on_review_of_review}}</td>
          <td>{{user.leaderboard_privacy}}</td>
        </tr>
...
</pre>
 
The startFrom:currentPage*pageSize portion of the filter calls the custom filter startFrom which simply splits the data to display based on the current page of pagination (currentPage), multiplied by the number of users to display (pageSize, which is set by the user selecting either All, 25, 50, 100, or 250).
 
In app/assets/javascripts/app.coffee:
<pre>
app.filter 'startFrom', ->
  (input, start) ->
    start = +start
    return input.slice start
</pre>
 
The limitTo:pageSize is a built in angular filter that says to stop displaying users after pageSize of them have been displayed.
 
The aforementioned pageSize is set by the user through a dropdown; This code tells the dropdown to set the default paginate value to 100 users per page, and upon an ng-change (user selecting a different value), the pagination method will be called in app.coffee, with the appropriate pageSize:
 
In views/users/list.html.erb:
<pre>
<label>Paginate By:
    <select class="form-control" ng-model="paginate" ng-init="paginate='50'" ng-change="pagination(paginate)">
      <option value="0">All</option>
      <option value="25">25</option>
      <option value="50">50</option>
      <option value="100">100</option>
      <option value="250">250</option>
    </select>
</label>
</pre>
 
This code is in app.coffee, and it posts the pageSize to the set_page_size method in the users_controller, and then sets various variable values based on the results:
 
In app/assets/javascripts/app.coffee:
<pre>
$scope.pagination = (ps) ->
    $http.post('/users/set_page_size', {
      'pageSize': ps
    })
    .success((value) ->
      $scope.pageSize = value[0]
      $scope.div = value[1]
      $scope.totalSize = value[2]
      )
</pre>
 
This code is in users_controller and it takes in the user defined pageSize variable and then determines how many pages are required to divide the data into based off of the pageSize, then returns that variable, plus others:
 
In app/controllers/user_controller:
<pre>
def set_page_size
    ps = params[:pageSize].to_i
    user = session[:user]
    role = Role.find(user.role_id)
    all_users = User.order('name').where( ['role_id in (?) or id = ?', role.get_available_roles, user.id])
    users_length = all_users.length
    if ps == 0
      pageSize = users_length
    else
      pageSize = ps
    end
 
    div = (users_length/pageSize.to_f).ceil
   
    respond_to do |format|
      format.html {render json: [pageSize, div, users_length]}
    end
end
</pre>
 
Once the number of pages and pageSize has been set, those numbers are then used to show the current and remaining pages to go along with the previous and next buttons:
 
In views/users/list.html.erb:
<pre>
<span id="paginate-button">
  <button class="glyphicon glyphicon-chevron-left" ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">
  </button>
  {{currentPage+1}}/{{div}}
  <button class="glyphicon glyphicon-chevron-right" ng-disabled="currentPage >= totalSize/pageSize - 1" ng-click="currentPage=currentPage+1">
  </button>
</span>
</pre>
 
3) Searching Users
 
[[File:Pag_src.png]]
 
In order to search users, a filter also needs to be set in order to limit the number of users being shown:
 
In views/users/list.html.erb:
<pre>
<tbody ng-repeat="user in users | filter:search | startFrom:currentPage*pageSize | limitTo:pageSize">
        <tr ng-click="showUser(user.object.id)">
          <td>{{user.object.name}}</td>
          <td>{{user.object.fullname}}</td>
          <td>{{user.object.email}}</td>
          <td>{{user.role}}</td>
          <td>{{user.parent}}</td>
          <td>{{user.email_on_review}}</td>
          <td>{{user.email_on_submission}}</td>
          <td>{{user.email_on_review_of_review}}</td>
          <td>{{user.leaderboard_privacy}}</td>
        </tr>
...
</pre>
 
The filter:search portion of this code simply applies the following filter criteria; the type of search is determined a dropdown (either search by any field, name, fullname, or email fields). Based on the dropdown selection, the correct search box will be shown, once that search box is show, the search criteria is set to correspond to the type of search specified by the dropdown:
 
In views/users/list.html.erb:
<pre>
  <div class="pull-right form-group">
  <div class="form-inline">
  <label>Search By:
    <select class="form-control" ng-model="searchtype" ng-init="searchtype='any'">
      <option value="any">Any</option>
      <option value="name">Name</option>
      <option value="fullname">Full Name</option>
      <option value="email">Email</option>
    </select>
  </label>
  <input class="form-control" ng-hide="searchtype != 'any'" ng-model="search.object.$">
  <input class="form-control" ng-hide="searchtype != 'name'" ng-model="search.object.name">
  <input class="form-control" ng-hide="searchtype != 'fullname'" ng-model="search.object.fullname">
  <input class="form-control" ng-hide="searchtype != 'email'" ng-model="search.object.email">
  </div>
  </div>
</pre>
 
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:
<pre>
<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>
</pre>
 
In app/assets/javascripts/app.coffee:
 
<pre>
  $scope.showUser = (user_id) ->
    $scope.user_display[user_id] = !$scope.user_display[user_id]
    $scope.editProfileVisible[user_id] = false
</pre>
 
As you can see in the screenshot bellow, as an example, user 6 has been selected and now the editing information about this user is visible.
 
[[File:select_user.png]]
 
 
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.
 
<pre>
  $scope.editUser = (user) ->
    # $scope.showUser(false)
    $scope.editProfileVisible[user.object.id] = !$scope.editProfileVisible[user.object.id]
    $scope.updatedUser = user
</pre>
 
The code above allows us to set the edit form visible. Bellow, you will find an example screenshot of this feature.
 
[[File:edit_user.png]]
 
 
=Resources=
 
<b>Github link:</b> https://github.com/CSC517-FInalProject/expertiza/tree/pei-final <br>
<b>Wiki link:</b> http://wiki.expertiza.ncsu.edu/index.php/CSC/ECE_517_Spring_2015_E1526_MPRI  <br>
<b>Project Demo:</b> https://youtu.be/qsHYE9Rgx80   <br>

Latest revision as of 06:33, 4 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)

The view resulting from this code structure can be seen bellow:


2) Paginating Users

In order to paginate users, a filter needs to be set in order to limit the number of users being shown:

In views/users/list.html.erb:

<tbody ng-repeat="user in users | filter:search | startFrom:currentPage*pageSize | limitTo:pageSize">
        <tr ng-click="showUser(user.object.id)">
          <td>{{user.object.name}}</td>
          <td>{{user.object.fullname}}</td>
          <td>{{user.object.email}}</td>
          <td>{{user.role}}</td>
          <td>{{user.parent}}</td>
          <td>{{user.email_on_review}}</td>
          <td>{{user.email_on_submission}}</td>
          <td>{{user.email_on_review_of_review}}</td>
          <td>{{user.leaderboard_privacy}}</td>
        </tr>
...

The startFrom:currentPage*pageSize portion of the filter calls the custom filter startFrom which simply splits the data to display based on the current page of pagination (currentPage), multiplied by the number of users to display (pageSize, which is set by the user selecting either All, 25, 50, 100, or 250).

In app/assets/javascripts/app.coffee:

app.filter 'startFrom', ->
  (input, start) ->
    start = +start
    return input.slice start

The limitTo:pageSize is a built in angular filter that says to stop displaying users after pageSize of them have been displayed.

The aforementioned pageSize is set by the user through a dropdown; This code tells the dropdown to set the default paginate value to 100 users per page, and upon an ng-change (user selecting a different value), the pagination method will be called in app.coffee, with the appropriate pageSize:

In views/users/list.html.erb:

<label>Paginate By: 
    <select class="form-control" ng-model="paginate" ng-init="paginate='50'" ng-change="pagination(paginate)">
      <option value="0">All</option>
      <option value="25">25</option>
      <option value="50">50</option>
      <option value="100">100</option>
      <option value="250">250</option>
    </select>
</label>

This code is in app.coffee, and it posts the pageSize to the set_page_size method in the users_controller, and then sets various variable values based on the results:

In app/assets/javascripts/app.coffee:

$scope.pagination = (ps) ->
    $http.post('/users/set_page_size', {
      'pageSize': ps
    })
    .success((value) ->
      $scope.pageSize = value[0]
      $scope.div = value[1]
      $scope.totalSize = value[2]
      )

This code is in users_controller and it takes in the user defined pageSize variable and then determines how many pages are required to divide the data into based off of the pageSize, then returns that variable, plus others:

In app/controllers/user_controller:

def set_page_size
    ps = params[:pageSize].to_i
    user = session[:user]
    role = Role.find(user.role_id)
    all_users = User.order('name').where( ['role_id in (?) or id = ?', role.get_available_roles, user.id])
    users_length = all_users.length
    if ps == 0
      pageSize = users_length
    else
      pageSize = ps
    end

    div = (users_length/pageSize.to_f).ceil
    
    respond_to do |format|
      format.html {render json: [pageSize, div, users_length]}
    end
 end

Once the number of pages and pageSize has been set, those numbers are then used to show the current and remaining pages to go along with the previous and next buttons:

In views/users/list.html.erb:

<span id="paginate-button">
  <button class="glyphicon glyphicon-chevron-left" ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">
  </button>
  {{currentPage+1}}/{{div}}
  <button class="glyphicon glyphicon-chevron-right" ng-disabled="currentPage >= totalSize/pageSize - 1" ng-click="currentPage=currentPage+1">
  </button>
 </span>

3) Searching Users

In order to search users, a filter also needs to be set in order to limit the number of users being shown:

In views/users/list.html.erb:

<tbody ng-repeat="user in users | filter:search | startFrom:currentPage*pageSize | limitTo:pageSize">
        <tr ng-click="showUser(user.object.id)">
          <td>{{user.object.name}}</td>
          <td>{{user.object.fullname}}</td>
          <td>{{user.object.email}}</td>
          <td>{{user.role}}</td>
          <td>{{user.parent}}</td>
          <td>{{user.email_on_review}}</td>
          <td>{{user.email_on_submission}}</td>
          <td>{{user.email_on_review_of_review}}</td>
          <td>{{user.leaderboard_privacy}}</td>
        </tr>
...

The filter:search portion of this code simply applies the following filter criteria; the type of search is determined a dropdown (either search by any field, name, fullname, or email fields). Based on the dropdown selection, the correct search box will be shown, once that search box is show, the search criteria is set to correspond to the type of search specified by the dropdown:

In views/users/list.html.erb:

  <div class="pull-right form-group">
  <div class="form-inline">
  <label>Search By: 
    <select class="form-control" ng-model="searchtype" ng-init="searchtype='any'">
      <option value="any">Any</option>
      <option value="name">Name</option>
      <option value="fullname">Full Name</option>
      <option value="email">Email</option>
    </select>
  </label>
  <input class="form-control" ng-hide="searchtype != 'any'" ng-model="search.object.$">
  <input class="form-control" ng-hide="searchtype != 'name'" ng-model="search.object.name">
  <input class="form-control" ng-hide="searchtype != 'fullname'" ng-model="search.object.fullname">
  <input class="form-control" ng-hide="searchtype != 'email'" ng-model="search.object.email">
  </div>
  </div>

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

As you can see in the screenshot bellow, as an example, user 6 has been selected and now the editing information about this user is visible.


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

The code above allows us to set the edit form visible. Bellow, you will find an example screenshot of this feature.


Resources

Github link: https://github.com/CSC517-FInalProject/expertiza/tree/pei-final
Wiki link: http://wiki.expertiza.ncsu.edu/index.php/CSC/ECE_517_Spring_2015_E1526_MPRI 
Project Demo: https://youtu.be/qsHYE9Rgx80