CS3226 Web Programming and Applications
30% Theoretical Test (Open Book)

S2 AY2016/17

Tuesday, 28 March 2017, 10.05-11.35am (90 minutes) (actually until 11.41am (96 minutes)) @ COM1-2-VCRm

My Matriculation Number: [A0][__][__][__][__][__][__][__]

The marks for each question is as indicated and the total for this test is 100 points which will be scaled to 30% afterwards.
For each question, please do not write more than the boxed space given.


  1. (3 marks)
    Mention at least three (3) applications that were previously exclusive desktop applications but now available as web applications (but still not as good as the desktop counterpart — elaborate this with a short reason)!










  2. Either recite https://visualgo.net/cs3226/introduction.html#/3/1 and https://visualgo.net/cs3226/introduction.html#/4/3 or give any other relevant answers. Current answers: Office software - ease of accessing files from thumbdrive, IDEs - for programmers, current online IDEs are not as powerful as the offline counterparts, Video/Photo editing software - need dedicated GPU/high-end PCs for good performance, heavy 3D game - similar performance reason, etc.

    Grading Remarks: Most students get at least 2+ (near full 3) points here as there are many possible answers for this supposedly giveaway question. Minority of you interpret 'desktop' application as the 'web application' via web browser and 'web application' is 'web application accessed via mobile devices'. That is incorrect. 'Desktop application' = offline application. Notice that I am not comparing 'desktop' vs 'mobile' although in the hindsight I should have said 'desktop (offline) applications' for 100% clarity.

  3. (2 marks)
    Write at least one potential scenario where you will ask your web server to issue an HTTP response status code 301 for a certain HTTP (GET) request to a resource in your website/web application.








  4. Any answer that makes sense will be accepted. During webserver lecture, Steven has mentioned one potential usage: Redirecting https://visualgo.net/sorting.html to shorter URL https://visualgo.net/sorting permanently so that the high search ranking of the older URL is maintained in the new URL (not in the slide, but I shown my VisuAlgo .htaccess file to class).

    Grading Remarks: Approximately two thirds of the class did not know what is HTTP 301 - Moved Permanently :(:(...

  5. In class, we have been shown that when we visit this long URL: https://visualgo.net/mst?create={"vl":{"0":{"x":100,"y":200},"1":{"x":200,"y":100},"2":{"x":300,"y":200}},"el":{"0":{"u":0,"v":1,"w":"1"},"1":{"u":1,"v":2,"w":2},"2":{"v":0,"u":2,"w":3}}}, we will get the following VisuAlgo view:





    (5 marks)
    Sub-questions:
    1). What is the name of the underlined part of the long URL above?
    2). What is the purpose of using that underlined part of the long URL?
    3). Why the underlined part of the long URL is not shown in the address bar eventually (after the page loads)?
    4). Which one is better: Remove the underlined part of the long URL after page load (see above) or leave it as it is?
    5). Assuming that the underlined part of the long URL is somewhat 'self explanatory', what should you change in that long URL if you want the picture of the triangle graph moves to the right by 100 pixels?

    1).

    2).

    3).

    4).

    5).

  6. 1). Query string, written in JSON notation.
    2). To provide a custom input (graph) for the visualization.
    3). There must be a JavaScript code that reads that query string, parse the JSON, and renders the graph as instructed by this JSON input, then use JavaScript API: window.history.pushState("object or string", "Title", window.location.href.split('?')[0]); to erase the query string part.
    4). VisuAlgo application allows user to further specify other input (see Draw Graph, Example Graphs) etc. As soon as the user loads another input graph, the long query string becomes meaningless/outdated. So it is better to erase it upon page load.
    5). Change the "vl" part to: {"vl":{"0":{"x":200,"y":200},"1":{"x":300,"y":100},"2":{"x":400,"y":200}}.

    Grading Remarks: For sub-question 2/3, some students say that this is server side parameters, which can be a valid interpretation too as not many knows that I process this input on VisuAlgo's client-side (using JavaScript). Most people get 4+ marks here.

  7. (5 marks)
    Please analyse the following Google Analytics dashboard screen shots of a real-life website/web application.

    Write as many insights as you can after seeing the screen shot above.
    Good/valid insights worth 1 mark each and weak/unclear insights worth 0.5 mark only.

    1).


    2).


    3).


    4).


    5).


  8. 1). Most (85%) visitors are from Singapore, so this must be a Singapore-based website/application.
    2). Most (75%) visitors are returning visitors, so this website must have a dedicated user base.
    3). There is a major dip around May-July 2016 and December 2016-January 2017 and periodical weekly patterns (6 weeks, 1 week dip, 7 weeks, 2 week dips, 1 week up...).
    4). That pattern is actually NUS semester 1 and 2 pattern :O.
    5). This is visitor profile of Steven's teaching website.

    Grading Remarks: Most students get 4+ marks here as long as you are not saying anything weird. Only 2-3 students guessed that this is NUSMods analytics or some kind of NUS-related teaching website.

  9. (4 marks). [Fictional scenario]: Suppose right now, when you visit URL https://visualgo.net using Google Chrome, we see the following scary warning: Your connection is not private... Attackers might be trying to steal your information from visualgo.net (for example, passwords, messages, or credit cards). NET::ERR_CERT_COMMON_NAME_INVALID.

    Sub-questions:
    1). What actually happened?
    2). If your opinion, do you think it is good for a web browser to enforce such check and then give such warning?
    3). How to resolve it?
    4). What is your advice to Steven so that his VisuAlgo project does not encounter this issue again in the future?

    1).

    2).

    3).

    4).

  10. 1). SSL certificate (usually of short duration) has expired.
    2). After knowing what damage an unsecure website can be (if involving user accounts/e-payment), I think it is a good idea for more and more web browsers to enforce this check, see this.
    2). Renew it; As Steven uses Digital Ocean with LetsEncrypt, this is as easy as running letsencrypt renew using root account of his web server that hosts VisuAlgo.
    3). Setup a cron-job for automatic renew periodically (e.g. every 2.5 months if the expiry date is per 3 months).

    Grading Remarks: About 1/5 of the class leave this (near) blank :(...

  11. (6 marks). Please observe the following screenshot of the sorting visualization of VisuAlgo when viewed on a small iPhone 5 device, portrait mode (left) and landscape mode (right).

    Write as many suggestions as possible on how to improve VisuAlgo views on mobile devices.
    The first suggestion has been written for you (as it is part of the next question).
    For the purpose of this question, just give suggestions using the shown sorting visualization as the context.
    Good/valid suggestions worth 1 mark each and weak/unclear suggestions worth 0.5 mark only.

    1). VisuAlgo visualizations are designed for landscape mode (more width than height). On portrait mode, everything looks too small. Therefore, encourage users to rotate their mobile device if the orientation is portrait (see the next question 7).
    2).


    3).


    4).


    5).


    6).


    7).


  12. 1). Already answered above.
    2). The top bar contains too many sorting algorithm names and causes layout overflow, consider shorten/rearranging the menu, or collapsing the nav-bar menu (hamburger style).
    3). We may need to hide some parts of the visualization on small mobile devices, e.g. maybe we have to hide the pseudo-code, the about/team/terms of use links?
    4). On mobile devices, show smaller number of integers to be sorted, e.g. the landscape mode is big enough for 7-8 numbers, but not more than that.
    5). There are precious screen real estate on mobile devices that are taken away by the black band surrounding VisuAlgo pages, consider turning them off for mobile view.
    6). Enlarge the (important) button sizes on small screen.
    7). Auto hide playback tool when animation is running, auto pause when tapped/clicked and show the playback tool again, etc.

    Grading Remarks: Many good suggestions although I cannot implement all. Average marks is around 4+ marks here with many of you left no 6/7 blank.

  13. (5 marks)
    Write a simple JavaScript/jQuery routine to implement the first suggestion from previous question 6.
    Event that you need to register: orientationchange to detect an orientation change.
    Properties that you need to compare: innerWidth/innerHeight properties of window object to get the window's height and width (not including toolbars/scrollbars).
    To simplify this question, please assume that we have an h1 element with text 'please view me on landscape mode' that has been positioned at the center of the screen, below the visualization element (see the portrait screenshot of question 6) and your job is to show/hide this element appropriately.










  14. function checkOrientation() { // define a function
      if ((window.innerHeight > window.innerWidth))
        $('h1').show(); // assuming that I only have one 'h1' element in the question
      else
        $('h1').hide();
    }
    window.addEventListener("orientationchange", function() { // register an event
      checkOrientation();
    }, false);
    checkOrientation(); // either put at the back or body.onload to test once upon load
    

    Grading Remarks: Many students only 'hide' the element once upon detecting landscape mode but never re-show it again as they did not register the event. 95% of students who register event do NOT test this portrait status during page load (which is not an orientation change event).

  15. (5 marks)
    You are handling a form that contains 3 fields: name, matric, and score.
    Before saving these three fields into database, you want to validate them first.
    name cannot be blank and must be at least 3 to 30 characters.
    matric cannot be blank and must be of real NUS matric number format, i.e. A0123456C (first character is always 'A', followed by 7 digits between [0..9], and ends with a character).
    score is not required, but if it is used, it must be an integer that is between [0..100].
    Complete the Laravel validator code below that you will use for this validation purpose.

    $validator = Validator::make($request->all(), [
      '                   ' => '                              ',
    
    
    
    
    ]);
    
  16. $validator = Validator::make($request->all(), [
      'name' => 'required|min:3|max:30',
      'matric' => 'required|regex:[A[0-9]{7}[A-Z]]',
      'score' => 'integer|min:0|max:100',
    ]);
    

    Grading Remarks: 1/3 does not get correct regular expression (used earlier in Lab4/HTML form validation). Among those who use Regex, a few repeates [0-9] literally 7x instead of using {7}. Many miss the 'integer' constraint and only say min:0|max:100 for 'score' that will allow non integers to be used.


    For question 9+10+11+12, you are given the following three Laravel blade files:

    // file: /resources/views/template.blade.php
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <title>CS3226 Test</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
        <style>
        </style>
      </head>
      <body>
        <div class="container">
          <div class="row">
            <div class="col-xs-offset-5 col-xs-2">
              @yield('picture')
            </div>
          </div>
          <hr>
          <div class="row">
            <div class="col-sm-8 col-xs-12">
              @yield('main')
            </div>
            <div class="col-sm-4 hidden-xs">
              @yield('aside')
            </div>
          </div>
          <hr>
          <div class="row">
            <div class="col-sm-12">
              @yield('comment')
            </div>
          </div>
        </div>
        <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
        @yield('script')
      </body>
    </html>
    
    // file: /resources/views/p1.blade.php
    @extends('template')
    
    @section('main')
    <h1>Story 1</h1>
    
    <p>This is the main story 1.
    </p>
    
    <ol start=7>
      <li>Topic 1,</li>
      <li>Topic 2,</li>
      <li>Topic 3.</li>
    </ol>
    
    <a class="btn btn-success" href="{{ url('/p2') }}">Go to page 2</a>
    @stop
    
    @section('aside')
    <h1>Background Story 1</h2>
    
    <p>This is side story 1
    </p>
    @stop
    
    @section('picture')
    <img class="img-responsive" src="cs3226-logo.png">
    @stop
    
    @section('comment')
    <p>This is a comment.
    </p>
    @stop
    
    @section('script')
    <script>
      $('div.row div').css('border', '1px solid')
                      .css('border-radius', '10px')
                      .css('border-color', 'red');
    </script>
    @stop
    
    // file: /resources/views/p2.blade.php
    @extends('template')
    
    @section('picture')
    <img class="img-responsive" src="cs3226-logo.png">
    @stop
    
    @section('main')
    <h1>Story 2</h1>
    
    <p>This is the main story 2.
    </p>
    
    <ul>
      <li>Topic 1,</li>
      <li>Topic 2,</li>
      <li>Topic 3,</li>
      <li>Topic 4.</li>
    </ul>
    
    <a class="btn btn-primary" href="{{ url('/p1') }}">Go to page 1</a>
    @stop
    
    @section('aside')
    <h1>Background Story 2</h2>
    
    <table class="table table-striped">
      <tr><th>No</th><th>Name</th></tr>
      <tr><td>1</td><td>Steven</td></tr>
      <tr><td>2</td><td>Zi Xian</td></tr>
      <tr><td>3</td><td>Mun Aw</td></tr>
    </table>
    @stop
    

    For question 9+10+11+12, you are also given the following file:

    // file: /routes/web.php
    <?php
    Route::get('/', function() { return view('p1'); });
    Route::get('p1', function() { return view('p1'); });
    Route::get('p2', function() { return view('p2'); });
    

    1. (8 marks)
      According to your current understanding of HTML5+CSS3+JS (jQuery)+Laravel Blade Templates, draw your best possible rendering of what will be seen if we enter localhost/test/public/p1 to the address bar of our web browser (assume that our Laravel project is at folder 'test' and we are testing it locally using a web browser of size 900x450 pixels).
      If needed, You can use text to describe color.
      Minor differences with the actual rendering done by Google Chrome are ignored.
      cs3226-logo.png is as shown below.

    2. What Steven is looking for (8 categories, -1 mark for each mistake found):

      1. The title is correctly written
      2. The logo is at first row, center
      3. There are two horizontal lines
      4. Story 1 on the left and Background Story 1 on the right, with approximately 2/3 and 1/3 width
      5. All divs have red rounded borders (the rounded part is not checked)
      6. The button 'Go to page 2' is colored green
      7. The list starts from number 7
      8. There is a comment

      Grading Remarks: Very few student is able to get all 8 tested marks correct but the average is around 5-6 marks.

    3. (2 marks)
      In short, what will we see if we enter localhost/test/public/?







    4. Same as localhost/test/public/p1

      Grading Remarks: 95% answered this correctly. A few said 'directory listing'... >.<.

    5. (8 marks)
      Now what will we see if we enter localhost/test/public/p2?

    6. What Steven is looking for (8 categories, -1 mark for each mistake found):

      1. The title is correctly written
      2. The logo is at first row, center (two horizontal lines are no longer checked)
      3. Story 2 on the left and Background Story 2 on the right, with approximately 2/3 and 1/3 width
      4. There is a table of CS3226 TAs on the right, striped
      5. All divs have NO red rounded borders
      6. The button 'Go to page 1' (the default color blue is ignored)
      7. The list is a bullet list with 4 items
      8. There is NO comment

      Grading Remarks: Similar to Question 9, very few student is able to get all 8 tested marks correct but the average is around 5-6 marks.

    7. (2 marks)
      In short, what will we see if we enter localhost/test/public/p3?






    8. We will see Laravel's default 404: 'Sorry, the page you are looking for could not be found.' as such route does not exist.

      Grading Remarks: 99% answered this correctly :).


    For the last 45 marks, you are given the following brief of a new but simple web application:

    Steven wants to revamp his currently broken Methods to Solve webpage to promote his 'Competitive Programming' (CP) book, 4th edition, to be released in 2020 just before International Olympiad in Informatics (IOI) 2020 in Singapore.
    This webpage should contains a long list of Steven's 2500+ solved programming problems in two online judges: University of Valladolid (UVa) online judge and Kattis online judge.
    Basically, the main part of the landing/index page is a huge sort-able/search-able/filter-able table (of 2500+ rows) that contains the following fields/columns that are read only for everyone in the world:

    1. oj: [UVa|Kattis], currently, Steven concentrates on these two online judges only,
    2. id: [5-digits UVa problem number|50-characters Kattis problem id without whitespace], for example, UVa: 00100, 01212, 10878, 10911, Kattis: 'substrings', etc.
    3. title: Maximum 100 characters, otherwise the long problem title will be truncated from the 98th character onwards and will be suffix-ed with a '...',
    4. section: In Ch.Se format where Ch(apter) ranges from [1..9] and Se(ction) ranges from [1..40].
    5. category: Maximum 30 characters.
    6. hint: Maximum 300 characters, this is the selling point of the CP book where Steven gives a short hint per problem.

    Long time ago, this 'Methods to Solve' webpage is 'read only' (written using static HTMLs).
    Steven wants to have a single user account (only for himself) that he can use to authenticate himself to this simple application.
    Upon authentication, he wants to have the capability to add new row/edit existing row/remove existing row on top of standard read (2500+) rows feature above for non-authenticated users.
    This way, he can dynamically update his 'Methods to Solve' webpage just via web browser after solving a new problem.


    1. (15 marks)
      Use the boxes below to draw simple mock ups of the public versus Steven's (the only admin) views of this new 'Methods to Solve' application!
      Please assume that each box is a 1280x800 web browser window.
      You can be as creative as possible.

      index.blade.php, Public view, read only, but can loginindex.blade.php, Steven's view after login, can CRUD
      create.blade.php (to create new row)edit.blade.php (to edit existing row)
    2. See the draft at https://cpbook.net/index2.php

      Grading Remarks: Most mockup drawings are relatively similar. I have taken note of the better design and will incorporate it on my new 'Competitive Programming' book website that I will revamp during May-July school holiday.

    3. (5 marks)
      Such a web-application has to utilize a persistent database to record these problem details that are editable by Steven.
      Suppose Steven has executed php artisan make:migration Create_Problem_Table --create=problems.
      Now edit database/migrations/[TIMESTAMP]_Create_Problem_Table.php appropriately so that he can run php artisan migrate afterwards.

      public function up() {
        Schema::create('problems', function(Blueprint $table) {
          $table->increments('id'); // Laravel default, auto increment
      
      
      
      
      
      
      
      
          $table->timestamps(); // another default: created_at, updated_at
        });
      }
      
      public function down() {
      
      
      }
      
    4. public function up() {
        Schema::create('problems', function(Blueprint $table) {
          $table->increments('id'); // Laravel default, auto increment
          $table->enum('oj', ['UVa', 'Kattis']); // just enum
          $table->string('id', 50); // use string and take the longer one as Kattis id can be up to 50 characters
          $table->string('title', 100); // max 100
          $table->float('section', 4, 2); // actually just number
          $table->string('category', 30); // max 30
          $table->string('hint', 300); // max 300
          $table->timestamps(); // another default: created_at, updated_at
        });
      }
      
      public function down() {
        Schema::dropIfExists('problems');
      }
      

      Grading Remarks: Many students do not specify the database constraints properly (not an optimized database schema if you do so).

    5. (5 marks)
      Suppose Steven has executed php artisan make:seeder ProblemSeeder.
      Complete the ProblemSeeder class below to insert at least these three valid entries:

      1. UVa, 10911, "Forming Quiz Teams", 8.4, "Dynamic Programming", "DP bitmask; graph matching",
      2. Kattis, substrings, "Repeated Substrings", 6.6, "Suffix Array", "clever usage of LCP information; interesting problem",
      3. UVa, 10878, "Flowery Trails", 5.3, "On Weighted Graph: Dijkstra's, Easier", "Dijkstra's but record predecessor graph (not just predecessor array) as there can be multiple shortest paths"

    6. <?php
      use Illuminate\Database\Seeder;
      class ProblemSeeder extends Seeder {
        public function run() {
      
      
      
      
      
      
      
      
        }
      }
      
      <?php
      use Illuminate\Database\Seeder;
      class ProblemSeeder extends Seeder {
        public function run() {
          // too lazy to enter the details, just insert the 3 given entries below
          DB::table('problems')->insert([
      
          ]);
          DB::table('problems')->insert([
      
          ]);
          DB::table('problems')->insert([
      
          ]);
        }
      }
      

      Grading Remarks: I am puzzled on why some of you put the entries in an array, and force yourself to use a loop (looping is what we will do if we use Faker class, but here I ask you to enter THREE specific seed entries).

    7. (2 marks)
      Next, Steven has executed php artisan make:model Problem.
      Complete the Problem class below:

    8. <?php
      namespace App;
      use Illuminate\Database\Eloquent\Model;
      
      class Problem extends Model {
      
      
      
      
      
      }
      
      <?php
      namespace App;
      use Illuminate\Database\Eloquent\Model;
      
      class Problem extends Model {
        // we do not need to set table name
        // as we already create table problems (plural of problem)
        protect $fillable = ['section','category','hint'];
        // we leave 'oj', 'id', and 'title' unchanged after creation
      }
      

      Grading Remarks: I am puzzled that 1/3 of the students leave this trivial question empty...

    9. (8 marks)
      Complete /resources/views/index.blade.php (the main landing page with the huge table)!
      Assume that we already have /resources/views/template.blade.php where we have instantiated Bootstrap and jQuery and have two sections 'main' (to replace the body part of the HTML document) and 'script' (at the end of the template).

    10. extends('template')
      @section('main')
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      @stop
      @section('script')
      
      
      
      
      
      
      
      @stop
      
      extends('template')
      @section('main')
      <div class="row">
        <div class="col-xs-12">
          <table id="problemtable" class="table table-condensed table-striped">
          <thead>
            <tr>
              <th>UVa</th><!--<th>Problem Title</th><th>CP3.17</th>-->
              <th>Hint</th>
            </tr>
          </thead>
          <tbody>
            <tr>
      <?php foreach ($problem_list as $p): ?>
              <td><?= $p->id ?></td>
              <td><?= $p->hint ?></td>
            </tr>
      <?php endforeach ?>
          </tbody>
          </table>
        </div>
      </div>
      @stop
      @section('script')
      <script>
      var table = $('#problemtable').DataTable({
        "bPaginate": true,
        "bAutoWidth": false,
        "aaSorting": [[0,'asc']],
        "lengthMenu": [[10, 20, 25, 50, 100, -1], [10, 20, 25, 50, 100, "All"]],
        "iDisplayLength": 20,
      });
      </script>
      @stop
      

      Grading Remarks: The one above is just a very coarse setup of the index.blade.php. It still missing the Auth part (for edit/delete — usually put at the row itself, and additional create button). For sorting/search/filter part, I am puzzled at why more than half of the class did not just mention the usage of DataTables and/or jQuery TableSorter that have been used in Lab1-6 (and asked in final exam of CP3101B two years ago).

    11. (10 marks)
      Complete /app/Http/Controllers/ProblemController.php below to support the following routes!
      You can assume that Steven has executed php artisan make:auth, have registered himself, then disable other Auth routes except login/logout.

      <?php
      // Auth::routes(); // not using the default
      $this->get('login', 'Auth\LoginController@showLoginForm');
      $this->post('login', 'Auth\LoginController@login');
      $this->get('logout', 'Auth\LoginController@logout');
      
      Route::get('/', 'ProblemController@index'); // Read, available for everyone, concentrate on this first
      Route::delete('problem/{id}', 'ProblemController@destroy'); // Delete, do this next
      Route::get('problem/{id}/edit', 'ProblemController@edit'); // Edit existing problem, show form
      Route::patch('problem/{id}', 'ProblemController@update'); // after completion of Edit form
      Route::get('problem/add', 'ProblemController@add'); // Create new problem/hint, show form
      Route::post('problem', 'ProblemController@create'); // after completion of Create form
      
    12. <?php
      namespace App\Http\Controllers;
      use Illuminate\Http\Request;
      use Illuminate\Support\Facades\Auth;
      use App\Problem;
      use Validator;
      
      class ProblemController extends Controller {
        public function __construct() { // fill this constructor
          $this->middleware('auth', ['except' => [                            ]]);
        }
        // add as many controller methods that you need
        // in this order: index (read), destroy (delete), edit/update, add/create
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      }
      
      
        public function __construct() {
          $this->middleware('auth', ['except' => [ 'index' ]]); // index is read only, no need to login
          // after passing this check to middleware auth, we do not need to specifically do that again below
        }
        // in this order: index (read), destroy (delete), edit/update, add/create
        public function index() {
          $problem_list = problem::all();
          return view('index', compact('problem_list'));
        }
        // ...
        // Steven himself haven't complete this but it should be just a few hours of work
        // as this app is very similar to our CS3226 Lab1-6 (literally very similar).
        // Steven is looking for find !!OrFail!! (otherwise crash if that id is not found)
        // proper usage of validator (or at least hint of using validator),
        // calling forms for edit/add and acting on that form input for update/create methods,
        // and ensureing that middleware auth is properly set up at __construct above
      }
      

      Grading Remarks: About half of the class leave this question blank (not enough time probably) despite this being a 'clone' of what you have seen and did in Lab1-6. For those who entered non blank answers, it is easy to get lots of marks by recalling what you did in Lab1-6.

    13. (Up to 5 additional bonus marks)
      Note that the sum of points up to Question 18 is already 100 marks.
      However, the simple web application above is not 100% complete yet.
      If you still have time, please use the extra two pages to write any remaining parts of this simple web application that have not been asked above for up to 5 additional bonus marks (maximum marks is still capped at 100 marks).

    14. Steven has not asked about Edit and Create form and any other misc stuffs. For Edit/Create, there can be option for 'bulk Edit' (i.e. I want to change all categories for N problems from category A to category B) and/or 'bulk Create' (i.e. I can upload a bunch of new problem entries using CSV file). All pages can also include navbar, etc...

      Grading Remarks: At the end, I get what I want. A test that has average ≥ 60%. The final average score is 64.8 with standard deviation of 10.2. That means, 68% of the class score around [64.8-10.2..64.8+10.2] = [54.6..75.0]. Anyone with score less than 54.6 should try to salvage some more marks by contributing more for your capstone project group. There are only a few students with score greater than 75.0 and these elite group should also try to lead their own group's capstone project to be a successful one.