Ancestry Pages: Development Notes

This page contains implementation notes for the features of this website. Notes on Bootstrap are here, and notes on jQuery and javascript are here.

Photos

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatum qui, earum eos omnis ab unde sapiente quam, fuga dolorem aliquam.

Log In System

In order to prevent unwanted changes to the database, a simple user log in system was implemented. Any data edit page -- a page that enables changes to be made to the data -- requires that the user be logged in. There is a single user, and there is no user interface for registering (the single user was inserted into the database manually).

A 'login' item was added to the menu bar. After the user has logged in, the item appears as 'logout', and the phrase 'logged in as name' appears at the right end of the menu bar.

The logged in status of the user is maintained in a PHP session variable ($_SESSION['loggedIn']). When each page in the site is rendered, a javascript variable is set based on the value of $_SESSION['loggedIn'], and the text of the login/logout menu item is set to the appropriate string.

<?php
  if ($_SESSION['loggedIn'])
    {
?>
  var loggedIn = true; 
  var userName = '<?php echo $_SESSION['userName']; ?>';
<?php
    }
    else
    {
?>
  var loggedIn = false; 
<?php
    }
?>
  // If logged in when page loads, put Logout on the menu, else put Login
  // Also set or clear the 'logged in as' item at the right side of the menu bar
  if (loggedIn) 
    {
      $('#logMenuItem').html('Logout');
      $('#userMenuItem').html('logged in as ' + userName);
    } 
    else 
    {
      $('#logMenuItem').html('Login');
      $('#userMenuItem').html('');
    }

When the login/logout menu item is clicked, if it says 'login', then the login form is displayed; if it says 'logout', then a PHP routine is called using ajax, and $_SESSION['userName'] is set to false.

    $("#logMenuItem").on("click", function() 
    {
      if ($(this).html()=='Login')
      {
        $('#loginModal').modal('show');
      }
      if ($(this).html()=='Logout')
      {
        $.ajax(
                'logout.php',
                {
                  type: 'POST',
                  success: function(data, status, xhr) 
                  {
                    alert('you have been logged out');
                    loggedIn = false;
                    $('#logMenuItem').html('Login');
                    $('#userMenuItem').html('');
                  },
                  error: function (jqXhr, textStatus, errorMessage){alert('Error in ajax logout call: ' + errorMessage);}
                });
      }
    });

When the login form is submitted a PHP routine is called to check the entered username and password.

    $('#loginform').on('submit', function(e)
      {
        e.preventDefault(); 
        var username = $('#username').val();
        var password = $('#password').val();
        $.ajax
        (
          'checkLogin.php', 
          {
          type: 'POST',
          data: { username: username, password: password },
          success: function(data, status, xhr) 
          {
            var rslt = data.substring(0, 1);
            if (rslt=='n') 
              {
                var msg = 'Invalid username and/or password';
                $('#invalMsg').html(msg);
              }
            if (rslt=='y')
              {
                var userName = data.substring(1);
                loggedIn = true;
                $('#loginModal').modal('hide');
                $('#invalMsg').html('');
                $('#logMenuItem').html('Logout');
                $('#logMenuItem').removeClass('logMeIn');
                $('#logMenuItem').addClass('logMeOut');
                $('#username').val('');
                $('#password').val('');
                $('#loginform').removeClass('was-validated');
                $('#userMenuItem').html('logged in as ' + userName);
              }
          },
          error: function (jqXhr, textStatus, errorMessage){alert(errorMessage);}
          }); // End of ajax call
      
      }); // End of respond to submission of the log in form.

The PHP code selects the record in the user table for the entered username (or returns 'n' if there is no such username in the database). If the user is found, the entered password is hashed using the PHP crypt function with the salt string that was used to hash the stored password. If the two hashes are equal, the routine returns 'y' followed by the name of the user.
  session_start(); 

  $rslt = 'n';

  $userIn = $_POST['username'];
  $pswdIn = $_POST['password'];

  //-----------------------------------------------------------
  // Code here to open the database...

  //-----------------------------------------------------------
  // Confirm that this username is registered
  $stmt = $pdo->query("Select * From ancUser Where username='$userIn'");
  if ($stmt->rowCount()==1)
  {
    $row = $stmt->fetch();

    //-----------------------------------------------------------
    // Set parameters for crypt
    $Blowfish_Pre  = '$2a$05$'; // Use blowfish for 5 rounds.
    $Blowfish_End  = '$';

    //-----------------------------------------------------------
    // Hash the input password and compare to the saved hash
    $hashOfInput = crypt($pswdIn, $Blowfish_Pre . $row['salt'] . $Blowfish_End);
    if ($hashOfInput == $row['password'])
    {
      $_SESSION['loggedIn'] = true;
      $_SESSION['userName'] = $userIn;
      $rslt = 'y' . $_SESSION['userName'];
    }
  }

  //-----------------------------------------------------------
  // Return the result
  echo $rslt;

Markdown Syntax Processing

Conversion of Markdown Here syntax to HTML is performed on the notes fields of an individual's documents, and on the text of their biography. A small subset of Markdown has been implemented, and one custom syntax has been defined and implemented.

markDown($theString, $doParas=false), which is in /data/codeLib.php, returns the result of converting all Markdown Here syntax in $theString to HTML. The $doParas parameter controls the way in which paragraphs are formatted as detailed in the "Line Breaks" section below.

Line Breaks

Before inserting paragraph tags, any \r\n sequences are converted to \n. Each occurance of a pair of new line characters then generates a paragraph break. If the $doParas parameter is true, each paragraph is styled with the bioP class, which uses a serif font and applies inter-paragraph spacing. $doParas is passed as true when generating the HTML for the biography page of an individual, but not when generating the biography preview on the family.php page, and not when generating the text of a note on an individual's documents page.

Emphasis

**some text** is converted to <strong>some text</strong>

Italics

*some text* is converted to <i>some text</i>

Popup Images

In order to enable biographies and document notes to include links that, when clicked, bring up a popup image window, we've extended the Markdown syntax for links. This syntax: @click me@(title|file) generates a button link that pops up an image window as described below in the Popup Images section. If the 'file' string does not start with 'http', the string 'http://brucewatkins.org/ancestry/images/' is prepended before the button code is generated.

Tables

about tables

Links Opening in a New Tab

Many document note fields include one or more links, e.g. to a page on Wikipedia or to a location on Google Maps. The hassle of typing the HTML for these links prompted the implementation of some Markdown syntax.

[click me](http://whatever), is converted to <a href="http://whatever">click me<a>. The code in the markDown function that processes Markdown link syntax is illustrative of the approach used to process each type of Markdown syntax, namely a while loop that does a regular expression match for the syntax, replacing the matched string with the equivalent HTML.

  //=============================================================
  // Links
  //  Convert [link text](linkUrl) into
  //    <a href="linkUrl" target="_blank">link text</a>
  //
  $pat = "\[([^\]]+)\]\(([^\)]+)\)";        // [link text](link url)
  $pat = '/^([^\[]*)' . $pat . '(.*)$/s';   // everythingBefore [link text](link url) everythingAfter
  while (preg_match($pat, $rslt, $matches))
  {
    $rslt = $matches[1] . 
            '<a href="' . $matches[3] . '" target="_blank">' . 
            $matches[2] . '</a>' . 
            $matches[4];
  }

Documents For an Individual

In the biography website for Dad, there is a hard-coded HTML table of supporting documents (birth, census, marriage, etc.) listed oldest to most recent. The MySQL database was created so that we could generate tables like that for any/each person in our family lines website.

The personDocsTbl function in codeLib.php, given an ID value for the ancPerson table, returns the HTML code for that person's documents table. The content of the table is retrieved from the database in two parts. An initial MySQL query pulls data for all columns except the links column containing links to document images; getting that data requires a second query.

The ancDoc table does have a field (idPerson) that points to the person that a document "belongs to", but we can not use that value when collecting all the documents for a person. We need to use the ancDocsOwned table. So the first query joins ancDocsOwned, ancDoc, ancPerson, and ancDocType, based on all the records in ancDocsOwned for the person in question.

We want to display the date in the form MMM DD, but the data is stored as a numeric month number and a numeric day number. So we convert the month number into a three character abbreviation using the MySQL str_to_date function and then taking the first three characters of the result: substring(Monthname(str_to_date(ancDoc.month,'%m')), 1, 3).

The first query gives us data for five of the six columns: year, date, type of document, who the primary document "owner" is, and notes. In order to prevent text wrapping, we use non-breaking spaces in the date, document type, and name columns.

 $sql  = '';
 $sql .= "Select Distinct ancDoc.ID As ID, ancDoc.year As Year, "                                             .  
         "Concat(substring(Monthname(str_to_date(ancDoc.month,'%m')), 1, 3), '&nbsp;', ancDoc.day) As Date, " . 
         "Replace(ancDocType.typeText, ' ', '&nbsp;') As Type, "                                              . 
         "Concat(ancPerson.fName, '&nbsp;', ancPerson.lName) As Who, "                                        . 
         "ancDoc.notes As Notes "                                                                             . 
         "From ancDocsOwned, ancDoc, ancPerson, ancDocType "                                                  . 
         "Where ancDocsOwned.idPerson=$personID "                                                             . 
         "And ancDoc.ID=ancDocsOwned.idDoc "                                                                  . 
         "And ancPerson.ID=ancDoc.idPerson "                                                                  . 
         "And ancDocType.ID=ancDoc.idDocType "                                                                . 
         "Order By ancDoc.year, ancDoc.month, ancDoc.day";
 $stmt = $pdo->query($sql);
 $results = $stmt->fetchAll();

The above MySQL select gives us a list of document ids associated with the person whose table we're constructing. We use that list of ids to construct a select statement for the ancImages table that pulls the link text and link URL for each image associated with the document. The resulting select statement:
Select idDoc, seq, linkText, linkURL From ancImages Where idDoc IN (id1, id2, ...) Order By idDoc, seq

Popup Images

The family lines page (family.php) has several links that, when clicked, open a modal popup window containing an image. E.g. in the brief bio for Peter Kirsch, there is a "the full image" link that shows his photo, and a "business card" link that shows the business card for the Frontier Hotel on Niagara Street.

Bootstrap provides classes and a sample template for creating a popup modal window. Early in the development of family.php, one of these popups was added every time a new popup image link was added to the page. It became clear that this approach was unweildly and certainly did not adhere to DRY.

What was needed was a single modal popup div and javascript code that, when an image link was clicked, moved the appropriate image (and title) into that div before the popup was displayed.

Here's the hidden div for the popup ...

 <div class="modal fade" id="imgPopUp" tabindex="-1" role="dialog" aria-labelledby="modalImgLabel" aria-hidden="true">
  <div class="modal-dialog modal-lg" role="document">
   <div class="modal-content">
    <div class="modal-header">
     <h5 class="text-center" id="modalImgLabel">image title goes here</h5>
     <button type="button" class="close" data-dismiss="modal" aria-label="Close">
      <span aria-hidden="true">×</span>
     </button>
    </div>
    <div class="modal-body" id="modalImg">
     <!-- img tag goes here -->
    </div>
    <div class="modal-footer">
     <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
    </div>
   </div>
  </div>
 </div>

... and an example of a link for displaying an image in the popup window ...

 <a class="button imgPopUp" 
    title="Peter Kirsch in his Knights of Saint John uniform"
    file="peterKbw.jpg"
    type="button" 
    data-toggle="modal">
    <span class="blue">the full image</span>
 </a>

... and the jQuery code that runs when the page loads. It installs an event handler that runs when a button with the class imgPopUp is clicked. The file and title attributes of the button are used as parameters to modify the HTML of the hidden modal window before making the window visible.

 var imgPre  = '<img src="images/';
 var imgPost = '" class="rounded mx-auto d-block onepxblack">';
 $(".imgPopUp").click(function () 
 {
  $("#modalImgLabel").html($(this).attr("title"));
  $("#modalImg").html(imgPre + $(this).attr("file") + imgPost);
  $("#imgPopUp").modal("show");
 });

CRUD System

As part of enhancing the site to include a back end database, a decision was made to automatically create pages for adding, displaying, modifying, and deleting records. In other words, to build an automated Create/Read/Update/Delete (CRUD) system.

The system uses the definition of the database tables to generate HTML and javascript code for tables and forms that display, create, and update records in the database. Relationships between tables are maintained. For example, if a record in table B is pointed to by a foreign key value in table A, then that table B record is not allowed to be deleted. Validation criteria for field values are also enforced when forms are submitted.

Metadata for table relationships and field validation criteria is stored in the structure of the MySQL data tables. Specifically, the metadata is stored in the Comments column of the table structure.

Six types of meta data strings are implemented that:

  1. indicate that the field is optional (the comment starts with =opt:)
  2. indicate that the field is a url (the comment starts with =url:)
  3. indicate that the field is a foreign key pointing to another table (the comment starts with =fkey:)
  4. indicate that the field is pointed to from another table (the comment starts with =parent:)
  5. provide a query that generates strings to populate a drop down (select) field in a form (the comment starts with =query:)
  6. provides a regular expression to be used to validate input values for the field.
A field may have several meta data strings, separated by a delimiter string (']delim[').

As an example, here is the structure of the ancPerson table:

Database

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus tempora consequuntur at, ipsum! A labore ad explicabo veritatis cupiditate sit totam beatae sapiente nulla, sint obcaecati perspiciatis dolorem modi iste.

text:

  code goes here

something else

You are logged in! log out