"Writing" a Journal

When I decided to start this journal I didn't expect to build the majority of the actual journal functionality myself, but that is exactly what I am doing. I built this website to share some of my technical experience and I think it's only fitting that my first entry describes the building of the journal itself.

Requirements

The first phase of development needed to address essential functionality that represents the minimum requirements to begin using the journal. The basic concept is to create a system that allows a user to quickly add interactive content to the website in the form of projects, articles and commentaries. Based on that idea I came up with the following list of requirements.

  1. Each journal entry must be associated to one or more Categories.
  2. Each journal entry must include a title, body, author and the date and time the entry was posted.
  3. Each author must include a name, email address and password, and optionally a website address.
  4. Each journal entry may contain zero or more comments.
  5. New comment entries require validation and anti-spam functionality.
  6. Journal entries may be added or modified using a web-based WYSIWYG editor.
  7. Ability to add comments may be enabled or disabled for each journal entry.
  8. Store all journal data in a MySql database, my host's preferred database solution.

Design

Interface

Journal Lists

Journal lists display a simple or summarized version of each entry filtered by predefined or user-defined categories. A summarized list includes title, a small portion of the body, the date the entry was created and the author. A simple list only includes the title and the date.

An ASP.NET Server Control was created to standardize the list display of journal entries. The definition for the JournalList class is shown in table 1. Currently, this is the only portion of the journal that is implemented as a stand-alone control, but eventually all journal functionality will be implemented in the same way.

Latsos.Web.UI.WebControls::JournalList
Inheritance
System.Web.UI.Control
Properties
name type description
CategoryFilter string[] An array of category names used to filter the list. All rows are returned if the array is empty or null.
CategoryURL string The URL of the page to display when a category link is selected. The query string "?categoryId={id}" is automatically appended to the URL.
DetailURL string The URL of the page to display when the an individual entry is selected. The query string "?postId={id}" is automatically appended to the URL.
DisplayCount int Limits the number of entries returned by the list. The default is -1 (all entries).
ListTitle string The title of the list (H1).
ListType ListTypeEnum Determines whether a summary or basic list is created. The default is Summary.

public enum ListTypeEnum { Basic, Summary }

Methods
name return type description
CreateChildControls() void Overridden. Adds controls to build the journal list. See implementation below.

table 1.

Journal Entry

The actual journal entry will be displayed using a basic aspx page that includes a PlaceHolder control for the journal entry content and related comments, and an "add comment" area for new comments. Future enhancements will include a "Related Entries" list in the right-hand column.

New comments may be added if the "Allow Comments" checkbox is selected for the individual entry. A Captcha challenge-response mechanism will be used to thwart automated spam attacks. The Captcha control I decided to use was written by Kevin Gearing.

Journal Editor

This is one of the few components that I did not write myself. I decided to use FCKEditor, an open source javascript application. The flexibility provided by this editor is incredible. It includes an ASP.NET component that allows easy integration into existing applications. The editor can be seen in figure 1.

figure 1.

Data Elements

The following entity diagram (figure 2) describes the tables and relationships used by the journal. The diagram is self-explanatory, except for a few items.

The password stored in the user table is encrypted using the Advanced Encryption Standard (AES). The user table includes a column named eV, which is the encryption initialization vector. The vector is used in conjunction with the encryption key (stored in a secure location) to encrypt and decrypt the password value. Storing a unique vector with each password makes it more difficult to break the encryption algorithm. For more information regarding this technique see Implementing Encrypted SQL Server Database Columns with .NET.

The postDate column in the post_category table is a calculated field used during filtering procedures. The expression for the field is Parent(post__post_category).postDate.

figure 2.

Data Access

The journal data is stored in a MySql database and is accessed using a custom library class (Latsos.Data.MySqlClient::MyData). The class builds a DataSet dynamically based on a simple XML file (figure 3) that describes the underlying database. All of the tables have a primary key that consists of a single column named "id".

xml version="1.0" encoding="utf-8"
<dd>
  <tables>
    <table name="post" primaryKey="id" />
    <table name="user" primaryKey="id" />
    <table name="comment" primaryKey="id" />
    <table name="category" primaryKey="id" />
    <table name="post_category" primaryKey="id" />
    <table name="relatedPost" primaryKey="id" />
  </tables>
  <relations>
    <relation 
      parent="category" 
      child="post_category" 
      foreignKey="categoryId" />
    <relation 
      parent="user" 
      child="post" 
      foreignKey="createdById" />
    <relation 
      parent="user" 
      child="comment" 
      foreignKey="createdById" />
    <relation 
      parent="post" 
      child="comment" 
      foreignKey="postId" />
    <relation 
      parent="post" 
      child="post_category" 
      foreignKey="postId" />
    <relation 
      parent="post" 
      child="relatedPost" 
      foreignKey="postAId" />
    <relation 
      parent="post" 
      child="relatedPost" 
      foreignKey="postBId" />
  </relations>
  <expressionColumns>
    <expressionColumn 
      tableName="post_category" 
      name="postDate" 
      typeName="System.DateTime" 
      expression="Parent(post__post_category).postDate"/>
  </expressionColumns>
</dd>

figure 3.

Development

Interface

Journal Lists

The JournalList class produces two types of lists, Basic and Summary.

Basic List markup:
<div class="journal">
  <h1>[list title]</h1>
  <ul class="onsite-list">
    <li><a href='journalEntry.aspx?postId=[post id]'></a>&nbsp[entry date]</li>
    ...
  </ul>
</div>

Summary List:
<div class="journal">
  <h1>[list title]</h1>
  <dl>
    <dt><a href='JournalEntry?postId=[post id]'>[entry title]</a></dt>
    <dd class="summary">[first paragraph of entry]</dd>
    <dd class="footer>
      <a href='JournalEntry?postId=[post id]'><img src="images/doc.gif"/></a>
      [entry date]
      [<a href='Journal.aspx?categoryId=[cat id]'>[cat name]</a>, ...]
      | posted by [author name]
    </dd>
    ...
  </dl>
</div>

protected override void CreateChildControls() { base.CreateChildControls(); //create main DIV Panel divJournal = new Panel(); divJournal.ID = this.ID; divJournal.CssClass = "journal"; Controls.Add(divJournal); //add list title HtmlGenericControl h1 = new HtmlGenericControl("h1"); h1.InnerHtml = _ListTitle; divJournal.Controls.Add(h1); //get journal dataset MyData myData = new MyData(Context.Server.MapPath(ConfigurationSettings.AppSettings["DDFile"])); //initialize variables DataTable dtCat = myData["category"]; DataTable dtPost = myData["post"]; DataRow[] postRows; int iCount = _DisplayCount; //create row array based on category filters if (_CategoryFilter.Length != 0) { string filter = "[name] IN ('" + String.Join("', '", _CategoryFilter) + "')"; DataRow[] catRows = dtCat.Select(filter, "[name]"); if (catRows.Length == 0) { divJournal.Controls.Add(new LiteralControl("<p class='empty'>No Entries Found</p>")); return; } string[] catIds = new string[catRows.Length]; for (int i = 0; i < catRows.Length; i++) catIds[i] = catRows[i]["id"].ToString(); DataRow[] postCategoryRows = myData["post_category"].Select("categoryId in (" + String.Join(", ", catIds) + ") AND Parent(post__post_category).isPublic = 1", "postDate DESC, postId"); if (postCategoryRows.Length > 0) { ArrayList alpcRows = new ArrayList(); alpcRows.Add(postCategoryRows[0]); int pcPostId = Convert.ToInt32(postCategoryRows[0]["postId"]); for (int i = 1; i < postCategoryRows.Length; i++) { if (pcPostId != Convert.ToInt32(postCategoryRows[0]["postId"])) alpcRows.Add(postCategoryRows[i]); } if (iCount == -1 || iCount > alpcRows.Count) iCount = alpcRows.Count; postRows = new DataRow[iCount]; for (int i = 0; i < iCount; i++) postRows[i] = (alpcRows[i] as DataRow).GetParentRow("post__post_category"); } else { iCount = 0; postRows = new DataRow[0]; } } //no category filters else { postRows = dtPost.Select("isPublic = 1", "postDate DESC"); if (iCount == -1 || iCount > postRows.Length) iCount = postRows.Length; } if (_ListType == ListTypeEnum.Summary) CreateSummaryList(divJournal, postRows, iCount); else CreateBasicList(divJournal, postRows, iCount); } private void CreateSummaryList(Panel divJournal, DataRow[] postRows, int iCount) { //add list entries HtmlGenericControl dl = new HtmlGenericControl("dl"); divJournal.Controls.Add(dl); for (int i = 0; i < iCount; i++) { DataRow postRow = postRows[i]; HtmlGenericControl dt = new HtmlGenericControl("dt"); dt.InnerHtml = "<a href='" + _DetailURL + "?postId=" + postRow["id"].ToString() + "'>" + postRow["title"].ToString() + "</a>"; dl.Controls.Add(dt); HtmlGenericControl dd = new HtmlGenericControl("dd"); dd.Attributes["class"] = "summary"; string bodyText = postRow["body"].ToString(); int iStart = bodyText.IndexOf("<p"); int iEnd = bodyText.IndexOf("</p>"); string bodyFragment = bodyText.Substring(iStart, iEnd - iStart + 4); dd.Controls.Add(new LiteralControl(bodyFragment.Insert(iEnd, "&nbsp;<a href='" + _DetailURL + "?postId=" + postRow["id"].ToString() + "'>read more</a>"))); dl.Controls.Add(dd); dd = new HtmlGenericControl("dd"); dd.Attributes["class"] = "footer"; DataRow[] postCategories = postRow.GetChildRows("post__post_category"); string[] cats = new string[postCategories.Length]; for (int c = 0; c < postCategories.Length; c++) { string catName = postCategories[c].GetParentRow("category__post_category")["name"].ToString(); cats[c] = "<a href='" + _CategoryURL + "?categoryId=" + postCategories[c]["categoryId"].ToString() + "'>" + catName + "</a>"; } string authorName = postRow.GetParentRow("user__post")["name"].ToString(); string editIconHtml = String.Empty; if (Convert.ToBoolean(Context.Session["isAdmin"])) editIconHtml = ""; dd.InnerHtml = editIconHtml + &quot; + postRow[&quot;title&quot;].ToString() + &quot; " + Convert.ToDateTime(postRow["createdOn"]).ToString("MMMM d, yyyy") + " [ " + String.Join(", ", cats) + " ] | posted by " + authorName; dl.Controls.Add(dd); } }

private void CreateBasicList(Panel divJournal, DataRow[] postRows, int iCount)
{
  HtmlGenericControl ul = new HtmlGenericControl("ul");
  ul.Attributes["class"] = "onsite-list";
  divJournal.Controls.Add(ul);
  for (int i = 0; i < iCount; i++)
  {
   DataRow postRow = postRows[i];
HtmlGenericControl li = new HtmlGenericControl("li");
   li.InnerHtml = "<a href='" + _DetailURL + "?postId=" + postRow["id"].ToString() + "'>" + postRow["title"].ToString() + "</a>&nbsp" + Convert.ToDateTime(postRow["createdOn"]).ToString("MMMM d, yyyy");
    ul.Controls.Add(li);
  }
}

Journal Entry

The journalEntry.aspx page's OnPreRender event is used to display the journal entry.  The page contains a PlaceHolder control (phEntry), which is populated with the journal entry title, body and related comments.

  protected override void OnPreRender(EventArgs e)
  {
    HtmlGenericControl h1 = new HtmlGenericControl("h1");
    h1.InnerHtml = _PostRow["title"].ToString();
    phEntry.Controls.Add(h1);

    HtmlGenericControl p = new HtmlGenericControl("p");
    p.InnerHtml = _PostRow["body"].ToString();
    phEntry.Controls.Add(p);

    DataRow[] commentRows = _PostRow.GetChildRows("post__comment");
    if (commentRows.Length != 0)
    {
      comments.Controls.Add(new LiteralControl("<h4>" + commentRows.Length.ToString() + (commentRows.Length==1?" comment</h4>":" comments</h4>")));
      HtmlGenericControl dl = new HtmlGenericControl("dl");
      comments.Controls.Add(dl);
      for (int i = 0; i < commentRows.Length; i++)
      {
        HtmlGenericControl dt = new HtmlGenericControl("dt");
        dt.ID = "comment" + commentRows[i]["id"].ToString();
        dl.Controls.Add(dt);
        dt.Attributes["class"] = "alt" + Convert.ToString(i % 2);
        string author =commentRows[i]["authorName"].ToString();
        string webSite =commentRows[i]["authorWebsite"].ToString();
        if (webSite != String.Empty)
          author = "<a href='" + webSite + "'>" + author + ">";
        DateTime dtime = Convert.ToDateTime(commentRows[i]["createdOn"]);
        dt.Controls.Add(new LiteralControl("" + Convert.ToString(i + 1) + ". at " + dtime.ToString("h:mm tt") + " on " + dtime.ToString("MMM dd, yyyy") + ", " + author + " wrote:"));
        HtmlGenericControl dd = new HtmlGenericControl("dd");
        dd.Attributes["class"] = "alt" + Convert.ToString(i % 2);
        dl.Controls.Add(dd);
        dd.InnerHtml = commentRows[i]["body"].ToString();
      }
    }
    base.OnPreRender (e);
  }
Journal Editor

The editor interface consists of three areas: Journal Entry, Categories and Options.  The Journal Entry area contains a text input control for the title, and the editor control that captures the body of the entry.  The title and body controls are initialized during the page Load event.  The Categories area displays a list of checkboxes which allow the author to easily identify the appropriate categories for the current entry.  The Options area includes a checkbox to indicate whether comments can be added to the entry, and a checkbox indicating that the entry is ready to publish.

1 comment

1. at 4:39 AM on Sep 06, 2010, Ann wrote:
Thanks for information!

Related Entries

no entries found