MovieManWeb

The purpose behind MovieManWeb as presented here is to provide an easy-to-follow example of how to use vibe.d. The focus should be largely on vibe.d itself, not on the vagaries of web development, aspects of good database design, or how to make the app as robust and feature-rich as possible. Toward that end, there are a few constraints we'll keep in mind as we go along that wouldn't apply to most vibe.d apps:

  • The application is intended to run on the desktop for a single user. There is no need to handle login credentials, hash passwords, sanitize input, or implement any such security features an application intended for deployment on the web would normally require.
  • The only data the application is concerned with are movie titles, case numbers, page numbers, disc numbers and, for television shows, season (or series) number. This constraint allows for a simple, single-table database, with no need to worry about efficient database design.
  • The output of the application is pure HTML5, with no scripting or fancy effects. No attempt is made to accommodate older browsers. A stylesheet is provided with the downloadable source code to give the user interface a more pleasing appearance, but it is not covered in the chapter. Given the first constraint, the default stylesheet assumes a desktop monitor and does not apply any responsive web design techniques.

These constraints may make MovieManWeb sound like it isn't a web app, but it absolutely is. With the implementation of the proper security precautions, a more flexible stylesheet, more user-friendly features in the UI and, optionally, more data to store (such as movie directors, actors and actresses, and so on), MovieManWeb could serve as the core of a more feature-complete, multi-user web application. Here's a look at the finished product:

MovieManWeb

Getting started

Before generating the dub project, Linux and Mac users will have to go through some preliminary steps. On Linux, you'll probably need to install libevent through your package manager (sudo apt-get install libevent libevent-dev on Debian-based systems) if it isn't already installed. On Mac, you'll have to get the source from https://github.com/libevent/libevent and follow the instructions to build using autoconf in the README file. If you get compiler errors on Mac and have no access to a Windows or Linux system, an alternative is to install Linux on a VM and follow along that way.

The first step in setting up the project is to tell DUB to initialize a new vibe.d app. Navigate to $LEARNINGD/Chapter10 and execute the following command:

dub init MovieManWeb -t vibe.d

That will create the MovieManWeb directory and populate it with the default contents we saw earlier. To make sure there aren't any networking issues on your system, cd into the MovieManWeb directory and execute dub with no arguments to compile and run the program (or dub -ax86_64 for a 64-bit build). Then open up your preferred web browser and enter either localhost:8080 or 127.0.0.1:8080 into the address bar. If you see the default "Hello, World!" output, then all is well.

Tip

Windows and the loopback address

127.0.0.1, or localhost, is known as the loopback address. In Windows 10, typing 127.0.0.1 into a browser address bar will work as expected, but typing localhost will not. This is because it is disabled by default. If you want to enable it, you'll need to open the file C:WindowsSystem32driversetchosts and uncomment one or both of these lines:

#  127.0.0.1       localhost
#  ::1             localhost

To uncomment, delete the hash tags (#) from the front of each line. Because this is a system file, you will need administrator privileges to save the changes. If you have an administrator account, you can right-click on the shortcut to a text editor and choose Run as administrator. Alternatively, you can drag a copy of the file from Windows Explorer to the desktop, edit the copy, and then drag it back to the original location. Windows will prompt you for administrative permission before completing the copy.

Next, open up MovieManWeb/source/app.d in your text editor. You'll want to replace the default low-level example function with the initial version of the high-level web interface we'll be using. Delete the following highlighted lines:

import vibe.d;

shared static this()
{
  auto settings = new HTTPServerSettings;
  settings.port = 8080;
  settings.bindAddresses = ["::1", "127.0.0.1"];
 listenHTTP(settings, &hello);

  logInfo("Please open http://127.0.0.1:8080/ in your browser.");
}

void hello(HTTPServerRequest req, HTTPServerResponse res)
{
  res.writeBody("Hello, World!");
}

Soon, we'll change listenHTTP to do something a bit different. Now it's time to create the web interface. Create a new directory, MovieManWeb/source/mmweb, and inside of it a new source file, web.d. The content of this file should look like this:

module mmweb.web;
import vibe.vibe;

final class MovieMan {
  void index() {
    render!("index.dt");
  }
}

Here, index.dt is the name of a diet template that we'll create in the views folder. We'll have an introduction to the mechanics of the web interface and diet templates shortly. For now, let's focus on getting something up and running. Create a new file and save it as MovieManWeb/views/index.dt. Implement the following:

doctype html
html
  head
    title MovieManWeb
  body
    h3 Hello, MovieManWeb!

That's much nicer to look at than raw HTML, isn't it?

We're almost ready to launch. To put the last pieces in place, go back to app.d and make the following highlighted changes:

shared static this()
{
  import mmweb.web : MovieMan;
  auto router = new URLRouter;
  router.registerWebInterface(new MovieMan);

  auto settings = new HTTPServerSettings;
  settings.port = 8080;
  settings.bindAddresses = ["::1", "127.0.0.1"];
  listenHTTP(settings, router);
}

Now you've got a customized minimal vibe.d HTTP web app. Execute dub in the MovieManWeb directory and reload 127.0.0.1:8080 to see the following result:

Getting started

To stop the server, you'll need to go back to the console window and press the Ctrl + C key combination on your keyboard. The server will then cleanly shut down.

The basics of diet templates

Looking at the index.dt presented in the previous subsection, one thing stands out quite clearly. Each line begins with a tag that maps directly to an HTML tag, only there are no brackets and no corresponding closing tags. The template engine is able to understand how to properly generate HTML tags from the input through indentation and new lines. Add the following highlighted lines to index.dt, then recompile the app and reload the web page:

doctype html
html
  head
    title MovieManWeb
  body
    h3 Hello, World!
    p This is a paragraph.
    p This is a new paragraph with a link to
    a(href="http://google.com")Google
    | and more text following the link.

The reason we need to recompile before seeing the changes is because diet templates are parsed at compile time. The generated HTML is then loaded into memory when the application launches, so that nothing is ever loaded from disk at runtime. It's still possible to serve static files from disk, as we'll see a bit later in the chapter, but most, if not all, of the UI in a vibe.d app will usually be generated at compile time. The result of the two new lines can be seen in the following screenshot:

The basics of diet templates

In this subsection, we're going to talk first about the interaction between tags and indentation, then we'll see how to combine multiple diet templates into a single generated HTML page. We'll finish off by setting up a common layout for every page MovieManWeb generates. Later in the chapter, when we actually have data to display, we'll look at another feature of Diet templates that allows us to generate output at runtime.

Tags and indentation

The parser uses indentation to determine when to insert a closing tag for any open tags that require them. Indentation can be via tabs or spaces and there is no required number of either; one space works equally as well as four, and two tabs work equally as well as one. When the parser first encounters an indented line, it will determine the type of indentation and will use that as the basis to understand the rest of the file. Whatever approach you choose for indentation, just be consistent. You can't use four spaces on one line and five on the next; the first indented line sets the pattern for each successive indented line and any deviation from the pattern will result in a compiler error.

Consider the very first line, doctype html. The parser understands that the equivalent HTML tag, <!DOCTYPE html>, has no corresponding closing tag and does not wrap any content. Therefore, the line following it need not be indented. However, the tag on the next line, html, corresponds to <html> and </html>, two tags between which all of the page content must exist. Every line following the second is indented, telling the parser that the content goes between those tags.

The third line is head, which corresponds to the HTML <head> and </head> tags. The parser sees that the fourth line, title, is indented, so it knows the output belongs between those two tags. It then encounters body, which is on the same indentation level as head. This is an indication to first insert the </head> tag in the generated content, followed by the opening <body> tag. The same logic is used when the p tags are on the same indentation as the h3 tag. By the end of the file, the parser sees that the body and html tags are still open, so it generates the </body> and </html> closing tags automatically.

Only tags need to be indented. Text content intended for output can appear on the same line as any tags to which they correspond. We can see this with the title and h3 tags, as well as the first p tag. The second paragraph contains a link to http://www.google.com. As such, the tag for the link needs to be indented. If you put it on the same line as the p tag, like so:

p Go to a(href="http://google.com")Google

Then the output will look like this:

Tags and indentation

The same thing applies to the paragraph text that follows the a tag. If it is on the same line as the tag, it will be included as part of the link, so we have to put it on the next line. However, if we just do this:

p This is a new paragraph with a link to
  a(href="http://google.com")Google
  and more text following the link.

The first word of the last line will not appear in the output. The parser treats the first word on every line as a tag. If it doesn't understand the tag, it just ignores it. In order to let the parser know that this is a line of text belonging to the preceding p tag and not a completely new tag, we have to use the pipe (|) character:

p This is a new paragraph with a link to
  a(href="http://google.com")Google
  | and more text following the link.

The indentation is also important. If the line is not one indentation level beyond the p tag, the parser will assume the paragraph should be closed, so it will generate a </p> HTML tag to make a new paragraph with the text. In effect, this…:

p This is a new paragraph with a link to
    a(href="http://google.com")Google
| and more text following the link.

…results in this:

Tags and indentation

Including and extending templates

If it were necessary to rewrite the same common tags for every diet template in the program, then the benefits of using the templates would be dramatically reduced. There are two ways to combine multiple templates to generate a single web page: we can include one template in another, and one template can extend another.

To include one template in another, the include directive should be used at the point in which the included template should appear. For example, given the template head.dt:

head
  title MovieManWeb

We can insert it into any other template like so, perhaps one called content.dt:

doctype html
html
  include head.dt
  body
    h3 Hello, World!

Don't forget about indentation rules; the include should be at the same indentation level at which the content would appear if it were typed in directly. When using includes, the included template is not given to the parser. In this example, the program would tell vibe.d to render content.dt.

Extending a template is like including one in reverse. Given a module sub.dt, which extends base.dt, sub.dt is fed to the parser. The parser will then take the entire content of the sub.dt and include it in base.dt to generate the final output. This requires two steps to be taken. For one, the extending template must include an extends directive at the top of the file. For example, the very first line of sub.dt should be:

extends base.dt

Additionally, the parser must be told where to insert the content of sub.dt into base.dt. Doing so requires block directives in both files. This implementation of base.dt shows how to do it in the template that is being extended:

doctype html
  head
    block title
  body
    block body

Block names need not be the same as the tags they belong to, nor are they required to be lowercase, but it's a convention I prefer.

Any template that extends base.dt will now need to include two blocks, one implementing the content that will replace the title block in base.dt, and another that will replace the body block. These blocks must be named the same as those in base.dt. Our sub.dt implementation might look like this:

block title
  title MovieManWeb - Page One
block body
  h3 Hello, MovieManWeb!

Again, the indentation rules apply at the point where all content from sub.dt is pasted into base.dt.

The MovieManWeb layout

While it's possible to use one diet template to represent multiple pages by inserting D code to generate different content for each page (as we'll see later), it's more manageable to use multiple templates. This is the approach taken in MovieManWeb. We will have one template for each page, with an additional layout.dt template that all of the others will extend. We will not use any includes. The complete implementation of layout.dt follows:

doctype html
html
  head
    title MovieManWeb
    link(href='/reset.css', rel='stylesheet')
    link(href='/style.css', rel='stylesheet')
  body
    nav
      ul
        li
          a(href='/') Home
        li Add New Movie
        li Find Movie(s)
    article
      block article

The stylesheets referenced in the link tags are located in the MovieManWeb/public directory of the downloadable source package. reset.css is a file released in the public domain and is available at http://meyerweb.com/eric/tools/css/reset/. It sets the properties of all HTML tags to a common state, allowing for custom styles to have a more consistent appearance across browsers. The file style.css is where the custom properties for MovieManWeb are implemented. When creating your own styles for the application, that is the file you should edit.

We are using the HTML5 nav and article tags, which are not supported by older browsers. An alternative is to use div tags with unique IDs. These can be generated with the long form div(id='nav'), or using the shortcut #nav, which generates the same output. Alternatively, div.nav and .nav are the same as div(class='nav'). If you choose to use div tags, then style.css will need to be edited appropriately.

With this in place, we can change index.dt to look like this:

extends layout
block article
  h3 Hello, MovieManWeb!

This file is going to become bigger as we progress. In the end, it will be the most complex template in the program, but will still be easy to follow.

In order for the stylesheets to be applied to the page, vibe.d has to be told the location from which it should serve static content. There are two functions in the module vibe.http.fileserver that can help with this. Both return a delegate that can be passed to a URLRouter during startup. One, serveStaticFile, is for associating a specific file with a URL. The other, serveStaticFiles (note the 's' at the end), is for associating a directory. The latter function works for our purposes. Modify app.d to include the following highlighted line:

shared static this()
{
  import mmweb.web : MovieMan;
  auto router = new URLRouter;
  router.registerWebInterface(new MovieMan);
  router.get("*", serveStaticFiles("./public/"));

  auto settings = new HTTPServerSettings;
  settings.port = 8080;
  settings.bindAddresses = ["::1", "127.0.0.1"];
  listenHTTP(settings, router);
}

The asterisk (*) tells the router that any URLs not already associated with any web interfaces or delegates should be handed off to the delegate returned by serveStaticFiles. The argument to that function, "./public/", means that any files in a URL the delegate handles should be searched for in the public directory.

With these changes in place, you can compile and run the app again to see the effect. Make sure to copy reset.css and style.css from the downloadable source package and place them in the MovieManWeb/public directory. Once that is done, reloading 127.0.0.1:8080 in the browser will produce the following output:

The MovieManWeb layout

Setting up the database

Now that we've got the basic layout configured, it's time to turn our attention toward the database layer. We need two things before we can continue: a data structure that can hold all of the data associated with a given DVD when going to and from the database layer, and a database with a table in which to store the data.

We're going to be storing more data for each movie than we did in the console version of MovieMan. This means that if we choose to represent each movie as a struct, it's going to be a few bytes larger, which can have an impact on the efficiency of passing instances around by value. In a large application, it would force careful consideration of whether any given function argument of that type should be passed by value or reference. If we choose instead to use a class, we will be required to instantiate new instances for every movie we pull from the database. In an application with heavy database usage, that could add quite a bit of garbage for the GC to clean up.

Realistically, it doesn't matter which approach we take for our single-user desktop web app. The number of database queries will be measured per minute at most, rather than per second, meaning the likelihood of generating enough garbage to clog up the GC is near zero. For the same reason, we need not worry about passing around high numbers of large struct instances by value. Regardless, we still need to choose one or the other.

As a general rule of thumb, any aggregate type that is intended to be a POD (Plain Old Data) type is better suited to be a struct than a class. Our Movie type is most certainly a POD type; it will consist of a number of member variables, with no member functions at all, and serves the sole purpose of carrying data between the database and the UI.

Now we have enough to create MovieManWeb/source/mmweb/db.d and enter the following:

module mmweb.db; 
import d2sqlite3;
struct Movie {
  long id;
  string title;
  int caseNumber;
  int pageNumber;
  int discNumber;
  int seasonNumber;
}

The sole database table will contain fields corresponding to each member of this structure. Notice that we're importing the d2sqlite3 module here so that we can use its symbols throughout the db module.

Creating the database is as easy as calling a class constructor. Here's how it's implemented:

_db = Database("./movieman.db");

Database is a struct type in d2sqlite3 that wraps a sqlite3 database handle. Creating the table for the database requires a bit of SQL. Since SQL is the focus of neither this chapter nor this book, we will not indulge in any explanations of the SQL statements we see. If you aren't familiar with SQL, the Kahn Academy tutorial is a good place to start. You can find it at https://www.khanacademy.org/computing/computer-programming/sql.

The SQL statements need to be strings. To make them more manageable and to keep the readability of db.d nice and clean, we'll declare a number of manifest constants in a separate module. Create a new module, MovieManWeb/source/mmweb/sql.d, and add the following:

module mmweb.sql;
package:
  enum createTableSQL =
  `CREATE TABLE IF NOT EXISTS movie (
    movieID INTEGER NOT NULL PRIMARY KEY,
   title TEXT NOT NULL,
    caseNum INTEGER NOT NULL,
    pageNum INTEGER NOT NULL,
    discNum INTEGER,
    seasonNum INTEGER
);`;

This is the SQL statement needed to create our database table. Because it spans multiple lines, note that it's implemented as a WYSISWYG string using backticks (`). We'll add more SQL statements to this module as we go along.

We'll implement a module constructor to do the work of creating both the database and the table. sqlite3 allows for database transactions, meaning that we can set things up such that any changes we make to the database can be rolled back on error. We will use transactions in every function that modifies the database to minimize the chance of database corruption. d2sqlite3 exposes the transaction API through the database object. The begin, commit, and rollback functions are what we are interested in. The first one starts a transaction, the latter two complete and abort it respectively. We can use scope statements to guarantee the appropriate functions are called.

With all of that in hand, we can add the following to db.d:

shared static this() {
  import mmweb.sql;

  // Create the database if it doesn't exist
  _db = Database("./movieman.db");

  // Begin a database transaction
  _db.begin();

  // Only commit the changes if no errors occur
  scope(success) _db.commit();

  // Abort on error
  scope(failure) _db.rollback();

  // Create the movie table if it doesn't exist
  _db.execute(createTableSQL);
}

Next, add the following to the bottom of the file:

private:
  Database _db;

There is at least one thing to do before this can compile, and more for those on Windows. First, we have to add the d2sqlite3 dependency to dub.sdl:

dependency "d2sqlite3" version="~>0.7.3"

This can go anywhere in the file, but I prefer to keep all dependencies grouped together, so I've inserted this right under the vibe.d dependency. If you are on any platform other than Windows, you can execute dub build now to make sure there are no errors and move on to the next subsection, Fleshing out the index page.

d2sqlite3 contains a line in its project configuration that will cause sqlite3 to automatically be linked in if it is on the system path. Under the hood, DUB will handle the compiler command line and make sure the appropriate naming convention is used. This is why users on Mac or Linux need not manually link with the library; no matter whether you're compiling for 32-bit or 64-bit architectures, the library will be installed to a location where the linker can find it. On Windows, the linker will be looking for sqlite3.lib on the system library path, but it's not going to be there unless you've put it there yourself (not recommended). The Windows ecosystem is not like that of other platforms.

To accommodate two different object file formats (COFF versus OMF) and two different architectures (32-bit and 64-bit), we need three different builds of the sqlite3 library on Windows. Windows users should open up app.d and add the following to the top of the file. It can go above or below the import statement:

version(Windows) {
  // When compiling with -m64
  version(Win64)
    enum sqliteLib = "lib\sqlite3-ms64.lib";
    // When compiling with -m32mscoff
  else version(CRuntime_Microsoft)
    enum sqliteLib = "lib\sqlite3-ms32.lib";
    // The default, or when compiling with -m32
  else
    enum sqliteLib = "lib\sqlite3-dm-dll.lib";

    pragma(lib, sqliteLib);
}

This will allow you to link to the appropriate build of sqlite3, no matter the linker or architecture you are compiling for. The first two are static libraries, but sqlite3-dm-dll.lib is an import library. It's used when compiling with the default architecture (32-bit) and linker (OPTLINK). All three libraries can be found in the MovieManWeb/lib directory of the downloadable source package. Additionally, sqlite3.dll is in the root MovieManWeb directory. Copy all four files over to your own project into the same locations. Now you can execute dub build to ensure everything is set up correctly for OPTLINK.

There's one more thing Windows users need to be aware of. DMD will issue a warning about the lib directive in d2sqlite3's project configuration, saying that sqlite3.lib cannot be found. No problem. The MS linker, unfortunately, is more strict and will issue a linker error instead. We can work around that, though. d2sqlite3 ships with two project configurations, one that includes the lib directive (the default) and one that does not (called without-lib). To specify the latter, add this to MovieManWeb's dub.sdl:

subConfiguration "d2sqlite3" "without-lib"

Now, you'll see no warning from DMD and no error from the MS linker when compiling with dub build –ax86_64. As I write, there is no command line flag to tell DUB to use the –m32mscoff switch telling DMD to link with the 32-bit MS linker. Adding it to the project configuration manually only results in errors. It will certainly be supported in a future version of DUB, hopefully one released not too long after this book.

Fleshing out the index page

The index page is always going to be used to display a list of movies. If there are no movies available in the database, it will simply display a message to inform the user that there are no movies to display. In order to make this happen, we need to embed some D code into the diet template.

To tell the parser to treat a given line in the template as code, we prefix the line with a dash (-):

-if(movies.length == 0)
  p There are no movies to display.

This snippet will only output the header if the movies array is empty. That raises the question, where does the movies array come from? To answer that, let's go back to our MovieMan web interface in web.d and modify the index function:

final class MovieMan {
  void index() {
    Movie[] movies;
    render!("index.dt", movies)();
  }
}

We've added two things. First, we've declared an empty array of Movie instances. Next, we've given that array to the render function. For completeness, we'll need to add this line to the top of the file:

import mmweb.db;

We'll need all of the database API to be available in the web module before we're through. Now let's look at what's going on with the index function.

Mapping web interface functions to URLs

When we give an instance of MovieMan to the registerWebInterface function in app.d, vibe uses compile-time reflection to map each member function to a URL. Any public function named index is mapped to the root URL /. Other public functions are mapped according to the format of their names. The URL will be /name, and the type of HTTP request it is mapped to is dependent upon the presence of any prefixes or @property annotations:

  • Functions annotated with @property that are getters (no parameters, return a value) are mapped to GET requests
  • Functions annotated with @property that are setters (one parameter, no return value) are mapped to PUT requests
  • Functions prefixed with get or query are mapped to GET requests
  • Functions prefixed with set or put are mapped to PUT requests
  • Functions prefixed with add, create, post, or no prefix at all are mapped to POST requests
  • Functions prefixed with remove, erase, or delete are mapped to DELETE requests
  • Functions prefixed with update or patch are mapped to PATCH requests

By far, the most common HTTP requests are GET and POST. The latter is normally used to submit form data, while the former is normally what is used when you type a URL in the browser's address bar or click on a link.

Tip

Be careful with case

In my testing, I was unable to determine how camel-cased function names, such as addMovie or postAddMovie, are mapped by default. The URLs /addMovie, /addmovie, and /AddMovie all resulted in 404 errors at runtime. However, addmovie and postAddmovie will map to /addmovie. In the end, I opted for one-word names in the web interface to keep it simple.

So, now we know that when vibe.d receives a request for the root page, it will call the index function on its instance of MovieMan to handle the request. In order for index.dt to have a movie array to work with, we need to pass one to the render template function.

Rendering diet templates

The vibe.web.web.render function template is a high-level wrapper of the lower level function template vibe.http.server.render. The latter requires an HTTPServerResponse as a parameter. When working with the low-level API, you have an HTTPServerResponse instance passed as a parameter to functions mapped to a URL. Using the web interface approach, the request and response objects are hidden behind the scenes, so vibe.web.web.render handles the response object for us.

Look again at the modifications to the index function:

Movie[] movies;
render!("index.dt", movies)();

Here, I've added empty parentheses to the end of the function call to make it as clear as possible that "index.dt" and movies are template arguments and not function arguments. Recall that the HTML output of index.dt is generated at compile time. Internally, vibe.d uses D's string mixins to take any lines of D code it finds in the template to turn it into compilable D code. All of this takes place when the compiler encounters this call to the render function and instantiates the template with the given parameters. Take this line out of the function, and the index page will never be generated.

Any variables that need to be handed off to the Diet template can be passed to the render function as template alias parameters after the filename. The name of the argument as it is passed to the function is the name that needs to be used in the Diet template. In this case, because the variable we are passing to render is named movies, then any D code in index.dt that needs to manipulate it must use the same name.

It's easy to forget during development what is and isn't visible inside a diet template. If you ever get confused, just keep two simple things in mind. First, if a variable is never passed to render together with the name of a Diet template, then that variable isn't visible in that template. Second, to make use of any D symbol, such as a function or an enum, a Diet template can import any module. For example:

-import std.string;

Rewriting index.dt

Now we've got almost all of the information we need to begin the real implementation of index.dt. As a first pass, it looks like this:

extends layout
block article
  h3 MovieMan Database
  -if(movies.length == 0)
    p There are no movies to display.

Note that the p tag is indented one level past that of the -if statement. Just as with everything else in a Diet template, indentation plays a major role in how the parser interprets lines of D code. If we did not indent the p line, the parser would assume it is not part of the –if statement, thereby including it in the final output no matter the state of the movies array.

To test this, go ahead and compile and run the server, then reload the page in the browser. The output looks like this:

Rewriting index.dt

We're going to be using index.dt to display movies in multiple circumstances. For example, when simply accessing the root URL, we will pull the first ten movies from the database and display them without any input from the user, providing links to show the next ten and, when appropriate, the previous ten. We will also use it to show the result of search queries and to display the result of adding a new movie or updating an existing one. That will require more programming logic in the template and a little more information to pass to the render function. We'll keep coming back to index.dt as we add more features.

Adding movies

Every feature we add to MovieManWeb is going to be more interesting to test if we have some actual data to test it with, so the first feature we'll implement is adding movies to the database. This will consist of the following steps:

  • Implement a function in db.d to add a movie to the database.
  • Implement a function in web.d that can map to a POST request handling data from an input form.
  • Implement a diet template that displays an input form where the user can enter movie data.
  • Modify index.dt to display the new movie.
  • Modify app.d to handle requests for the input form's URL.
  • Add a link to the input form in layout.dt.

Let's get to work.

Implementing the addMovie function

The addMovie function we'll add to db.d is going to require some support. We'll need to add an SQL statement to sql.d. In order to make the database queries more efficient (and our code a bit cleaner), we'll be using prepared statements, so we need to implement one for adding a movie. Let's start with the SQL.

Open up sql.d and add the following line:

enum insertSQL = "INSERT INTO movie(title, caseNum, pageNum, discNum, seasonNum) VALUES(?1, ?2, ?3, ?4, ?5);";

The numbers prefixed with question marks are placeholders for the variables we will insert into the prepared statement.

In d2sqlite3, prepared statements take the form of a struct type named Statement. Statements are created by calling the prepare member function of a database instance and passing it a string containing an SQL statement. When the Statement is ready to be used, variables are bound to the placeholders via their bind function, then the SQL is executed with the execute function. When all this is done, the Statement's reset function should be called before attempting to bind any variables to it again.

First, we need to declare a Statement instance in the private section of db.d. Add the highlighted line:

private:
  Database _db;
  Statement _insert;

Now we need to turn our attention to the module constructor in db.d. Here, we need to set _insert with a call to _db.prepare. Add the highlighted lines to the end of the module constructor:

  // Create the movie table if it doesn't exist
  _db.execute(createTableSQL);
  // Create the prepared statements
  _insert = _db.prepare(insertSQL);

Now we can implement the addMovie function. Because this function will modify the database, we'll want to use a database transaction. That calls for two scope statements. Here is the function in its entirety:

long addMovie(Movie movie) {
  _db.begin();
  scope(success) _db.commit();
  scope(failure) _db.rollback();

  _insert.reset();
  _insert.bind(1, movie.title);
  _insert.bind(2, movie.caseNumber);
  _insert.bind(3, movie.pageNumber);
  _insert.bind(4, movie.discNumber);
  _insert.bind(5, movie.seasonNumber);
  _insert.execute();

  return _db.lastInsertRowid();
}

The very first thing we do is begin the database transaction, then we set up the scope blocks just as we did in the module constructor. After that, the prepared statement is reset to prepare it for new values. Next, we bind the value of each member of Movie, excluding id (the database will generate one for us) to the corresponding placeholder in the prepared statement. After that, we execute the statement. Finally, we return the result of _db.lastInsertRowid. This will be used later in the web interface to fetch and display the newly added movie.

Note

Those familiar with SQL but not sqlite3 may have noticed in the createTableSQL constant that AUTO INCREMENT was missing from the declaration of the movieID field as a PRIMARY KEY. sqlite3 will automatically assign what it calls the rowid to any primary key field in a table if AUTO INCREMENT is not specified. If AUTO INCREMENT is specified, the database has to do a little more work. Therefore, it is recommended to only use AUTO INCREMENT when you absolutely cannot reuse any value as a primary key. Once an item is deleted from a table, its rowid may be recycled.

Implementing the postAdd function

The web interface function that handles the form for adding new movies is called postAdd. This means that when we implement the form, we will need to configure it to send its data to the /add URL using the POST method. It's a short function that essentially performs two tasks: it calls addMovie, then renders the index page with the appropriate information to display the newly added movie.

One of the conveniences of the web interface is that any form data or URL parameters are automatically pulled from the request and mapped to function parameters via compile-time reflection. This means that fields in the form should have names that match the parameters of the function that will handle the data. With that in mind, open up web.d and add the following to the MovieMan class:

void postAdd(string title, int caseNumber, int pageNumber, int discNumber, int seasonNumber) 
{
  auto id = addMovie(Movie(0, title, caseNumber, pageNumber, discNumber, seasonNumber));
}

By the time we're finished, index.dt is going to have a handful of variables it needs to properly render the movie list. Each function that calls render with "index.dt" as a template argument will need to pass all of those variables along. We can save ourselves some annoyance by wrapping them all in a struct, which we'll call ListInfo.

ListInfo contains four fields, only two of which we need to concern ourselves with right now, and one convenience constructor. Its implementation follows. It should be added to the top of web.d:

struct ListInfo {
  long offset;
  long limit = 10;
  Movie[] movies;
  ListType type; 

  this(ListType type) { this(0, type); }

  this(long offset, ListType type) {
    this.offset = offset;
    this.type = type;
  }
}

We'll worry about the offset and limit fields later. For now, the two members of interest are movies and type. The former is the array from which index.dt will pull all of the movies it needs to display. The latter is an enum that tells index.dt why it is being called. The code in the template will use this value to display a context-specific header. ListType, which you can also add to the top of web.d, looks like this:

enum ListType {
  generic,
  addMovie,
  findMovie,
  updateMovie,
}

With these two types in place, we can now complete the postAdd function by adding the following highlighted lines:

void postAdd(string title, int caseNumber, int pageNumber, int discNumber, int seasonNumber)
{
  auto id = addMovie(Movie(0, title, caseNumber, pageNumber,discNumber, seasonNumber));
  auto info = ListInfo(ListType.addMovie);
  info.movies = [Movie(id, title, caseNumber, pageNumber, discNumber, seasonNumber)];
  render!("index.dt", info);
}

info is initialized with the single argument constructor to set the type of the list for index.dt to read. We give info.movies a Movie instance constructed with the same parameters we added to the database. Technically, that's cheating. While it lets the user verify the information was entered into the form correctly, it doesn't allow for any corruption of the data that may have occurred when it was handed off to addMovies. The proper thing to do here is to use id to fetch the newly added information from the database and send that to index.dt for rendering. Since we don't have that functionality yet, this will do. Later, once we've added the ability to find a movie by ID, we'll come back and make the change.

While we're here in web.d, we also will need to modify the index function. Recall that we adjusted it earlier to pass an array of Movies to the render function. Since index.dt is going to be modified to work with ListInfo instances instead, index will also need to send one. If we don't change it, we won't be able to compile. Here, the ListType we need is ListType.generic:

void index() {
  auto info = ListInfo(ListType.generic);
  render!("index.dt", info);
}

Implementing add.dt

The input form consists of five pairs of input fields and labels in a simple table, with a button at the bottom. Create a file MovieManWeb/views/add.dt. Here's the boilerplate that goes on top:

extends layout
block article
  h3 Add a New Movie

Every Diet template we create for this application will have the same three lines at the top, with the text in the h3 tag being context specific. Next up is the form tag, which sits at the same indentation level as the h3 tag. We need to specify the action and method properties to trigger the postAdd function that we implemented previously:

    form(action='/add', method='post')

This is followed by the table tag, one indentation level beyond the form tag. For our implementation, we've given it a class called Form so that we can style it in style.css. In HTML, classes are specified as a property of the tag, such as action and method properties on the form tag, but in Diet templates we append classes to the tag name with a dot (.):

  table.Form

Finally, we have a series of six table rows (tr tags). The first five rows contain two cells (td tags) each. The first of each pair of cells contains a label identifying the input field in the second cell. The first input field is a text field; the remainder are all number fields. Each input field is given a size property for aesthetics. The final row contains a single cell that spans two columns. This cell contains the form's submit button.

It's important when viewing the content of add.dt to make sense of the indentation, given that it directly affects how the template is parsed. Unfortunately, formatting such things in a book can distort the text to a degree that makes it difficult to follow the original formatting. So to make sure you get the full picture, the content of add.dt is displayed here as an image, rather than as copy-and-pasted text. Other Diet templates will be displayed this way as we work through the chapter, where necessary. If you want to copy the text yourself, refer to the downloadable source code:

Implementing add.dt

Note that each input field has an id property to match the for property of its corresponding label. This is a feature of HTML5 that allows focus to shift to an input when its corresponding label is clicked, but is also important for usability and accessibility, for example, screen readers.

Modifying index.dt

Now we need to add two pieces of functionality to index.dt. First, is a check for the ListType it is intended to display. This will allow it to show the appropriate header. Next, we'll need to implement the logic to set up a table in which all of the movie data will be displayed. First up is the header. The highlighted lines are new:

extends layout
block article
  -import mmweb.web;
  -if(info.type == ListType.addMovie)
    h3 Sucessfully Added Movie
  -else
    h3 MovieMan Database
  -if(movies.length == 0)
    p There are no movies to display.

Note that the existing h3 tag, which reads MovieMan Database, has been indented one level past the else. Also note that we've imported mmweb.web. This is so we can have access to the members of the ListType enumeration.

The table we're going to implement has one column for each member of the Movie type and an additional column for the Edit and Delete links, for a total of seven columns. The first row consists of seven table headers (th tags), indicating the purpose of each column. We'll generate a new row for each Movie instance in the movies array via a foreach loop. For each instance, the value of each member is extracted and placed into the output via the !{} syntax, for example, to extract the value of the title field: !{movie.title}. Using the id field of each movie, links to the /edit and /delete URLs, the handlers for which we'll implement later, are generated and placed in the last cell of each row. All of the following highlighted code is new:

-if(info.movies.length == 0)
  p There are no movies to display.
-else
  table.MovieList
    tr
      th ID
      th Title
      th Case
      th Page
      th Disc
      th Season
      th Action
      -foreach(i, movie; info.movies)
        tr
          td !{movie.id}
          td.Title !{movie.title}
          td !{movie.caseNumber}
          td !{movie.pageNumber}
          td !{movie.discNumber}
          td !{movie.seasonNumber}
          td
            a(href='/edit?movieID=!{movie.id}') Edit
            br
            a(href='/delete?movieID=!{movie.id}') Delete

The first line, not highlighted since it isn't new, was modified so that the -if tests info.movies.length instead of movies.length. The table tag is given the MovieList class so that it can be styled to taste in style.css. Notice the second of the last three lines is a break (br) tag. Break tags should always be inserted at the same indentation level as the text they apply to. In this case, we want the Edit link to appear on one line and the Delete link to be on the next, so the br tag is at the same indentation level as both.

Modifying app.d

We've got everything in place to get the work done, but as yet the server has no idea how to display the input form. We don't have a function in the web interface to render it. The postAdd function is for handling the data from the form, not for displaying the form itself. We could add a new function to MovieMan to render add.dt, but there's another approach that works equally well and helps us keep MovieMan clear of functions that don't need any logic.

What we need to do is to tell the server how to associate a URL with a function that knows how to render add.dt. We can do that by registering a delegate with the get function of URLRouter, just like we did when we configured it to serve static files. The vibe.http.server module has a function, staticTemplate, which takes the name of a template and returns a delegate that will render it. So open up app.d and add the highlighted line:

router.registerWebInterface(new MovieMan);
router.get("/forms/add", staticTemplate!"add.dt");
router.get("*", serveStaticFiles("./public/"));

We could choose anything for the URL except / or /add, since those already map to the index and postAdd functions of MovieMan. However, since the form in add.dt is associated with the postAdd function that handles the /add URL, it's a good convention to keep add in the form URL so that we never lose track of how the related parts of the app match up. /forms/add allows us to do that.

Modifying layout.dt

Now the only step remaining is to give the user a way to access the input form. This requires one minor modification of layout.dt. We need to convert the second list item into a link that points at /forms/add. The following highlighted lines show the modifications:

ul
  li
    a(href='/') Home
  li
    a(href='/forms/add') Add New Movie
  li Find Movie(s)

All we've done is move the text of the second list item to a new line, add an anchor tag in front of it that points to the form URL, and indented it one level beyond the preceding li tag.

That's the last of the changes to add a movie to the database. Now you can launch the server, reload the page in the browser, click on the Add New Movie link, and add as many movies to the database as you like.

Tip

Invisible tables

If the table doesn't appear after adding a movie, try increasing the size of the browser window. If it still isn't visible, you'll probably need to tweak the stylesheet to be more flexible. Or, while testing, just remove the stylesheet links from layout.dt.

Listing movies

Recall that the intended default behavior of index.dt is to show a list of ten movies from the database. That's the ListType.generic flag. When the user loads the page for the first time, the index function in MovieMan should fetch the first ten movies from the database and hand them off to the render function. index.dt will display each movie and, depending on the context, add Next and/or Previous links to display more. To implement this functionality, the following steps are required:

  • Implement a function in the database layer that can fetch a certain number of movies, given a row offset and a count
  • Modify the index function to fetch movies from the database and pass them off to the render function
  • Modify index.dt to display Next and Previous links when appropriate

Implementing the listMovies function

The first thing to do on the database side is to set up an SQL statement to fetch a number of movies from the DB. Open up sql.d and add the following:

enum listSQL = "SELECT * FROM movie LIMIT ?1 OFFSET ?2;";

Again, we're going to use a prepared statement for this query. We have two parameters that we'll need to bind. The first is the limit, which specified how many database rows to return in the result set; the second is the row offset, namely, which row to pull first. An offset of 0 is the first row, an offset of 10 the first 10 rows, and so on. Although we won't implement it in this book, the database layer, the index function, and the table logic in index.d support limits other than 10. Adding the ability for the user to select the number of movies to display at once is a potential post-book exercise.

Next up, we need to get the prepared statement ready. Add the highlighted line to the private section of db.d:

Statement _insert;
Statement _list;

Then add a line to the module constructor to set it up:

// Create the prepared statements
_insert = _db.prepare(insertSQL);
_list = _db.prepare(listSQL);

Next, we're going to implement a helper function. listMovies isn't the only function in the DB layer that will need to return an array of Movies. Extracting all of the data from a d2sqlite3 ResultRange into a Movie instance is repetitive. To make it easier to manage, add the getResults function to the private section of db.d:

Movie[] getResults(Statement statement) {
  auto results = statement.execute();
  Movie[] movies;

  if(!results.empty) {
    movies.reserve(10);
    foreach(row; results) {
      movies ~= Movie(
        row.peek!long(0),
        row.peek!string(1),
        row.peek!int(2),
        row.peek!int(3),
        row.peek!int(4),
        row.peek!int(5)
      );
    }
  }
  return movies;
}

The statement is executed and, if the result range is not empty, a new Movie instance is populated with data and added to an array. The ResultRange is a range that contains Rows. A Row is a range that contains ColumnData, and those contain the data we are interested in. The Row type has a convenience function, peek, which takes an index as a function argument and a type as a template argument. It grabs the data at that index and converts it to the desired D type. Without peek, this loop would be much uglier, as we'd have to iterate over every Row and manually extract the data.

Finally, add the listMovies function to the public section of db.d:

Movie[] listMovies(long limit, long offset) {
  _list.reset();
  _list.bind(1, limit);
  _list.bind(2, offset);
  return _list.getResults();
}

The helper function allows listMovies to be nice and compact.

Modifying the index function

Now that listMovies is implemented, we can go ahead and use it in the index function. We're going to need to do more than just call listMovies, though. The function will always need to know which row offset should be the starting point of the list. We can't just always start with the first row. This means the index function will need to have a parameter.

Note

For those not familiar with HTTP GET and POST methods, each can send parameters to the server. For POST methods, the parameters are part of the packet of data the browser sends to the server and are never seen by the user. For GET methods, the parameters are appended to the URL like so: http://mysite.com?param1=2;x=3.

The parameter for the root page should always be optional, so that when you browse to http://127.0.0.1:8080 to see your app, you don't need to specify any parameters manually. To make that work, we can give the index function a parameter, which we'll call offset, and give it a default value of 0. That way, if the URL contains no parameters, vibe.d will see that we have assigned a default value to the function parameter and won't error out on us. When a parameter with no default value is missing from the request, vibe.d will always throw an exception (when using the web interface API).

With that information, we can open up web.d and modify the index function to look like this:

void index(long offset=0, long limit = 10) {
  auto info = ListInfo(offset, ListType.generic);
  info.limit = limit;
  info.movies = listMovies(limit, offset);
  render!("index.dt", info);
}

Modifying index.dt

Now all that remains is to add the Next and Previous links to index.dt. The way it works is this: We first test whether the ListType is ListType.generic, as other list types (in this book's implementation of MovieManWeb) aren't concerned with limits or Next and Previous links. If it is a generic list, we'll add another row to the table with a total of five cells, two of which span two columns. Inside the two longer cells, we'll implement some logic to either display the links or display nothing. This time, I'll show both an image and the new code. The image is shown first, so you can get a frame of reference, as follows:

Modifying index.dt

And now for the code:

-if(info.type == ListType.generic)
  tr
    td.NextPrev
    td.NextPrev
    -if(info.movies.length == info.limit)
      a(href='/?offset=!{info.offset + info.limit}') Next
    td.NextPrev(colspan='3')
    td.NextPrev
      -if(info.offset >= info.limit)
        a(href='/?offset=!{info.offset - info.limit}') Previous
        td.NextPrev

The new addition begins immediately after the code for the loop. The -if is on the same indentation level as the -foreach, because it follows it and is not part of it. Just to be clear, in normal D code it would look as follows:

foreach(i, movie; info.movies) {
  ...
}
if(info.type == ListType.generic) {
  ...
}

Putting the -if one indentation level beyond the -foreach would be the same as putting the if statement inside the foreach block.

The td tags all have the NextPrev class applied. Like the other CSS classes we've seen so far, this is to allow styling that's specific to these cells. Again, if you aren't happy with the style, modify style.css as much as you like.

The part that does the work is in the two -if lines inside the cells. The first one checks whether info.movies.length is greater than or equal to info.limit. This is a fairly good indicator that the Next link can be shown. If the test passes, a link will be generated with info.offset + info.limit as the offset parameter for the index function. Similar logic is used to decide whether or not to show the Previous link, testing info.offset against info.limit rather than the length of the movie array.

With all of that completed, you can recompile and relaunch the server, then reload the browser at 127.0.0.1:8080 to see all of the movies you added to the database after completing the previous subsection.

Finding movies

There are a few different keys we might like to use to search for a movie in the database. Internally, it can be useful to search by ID (to edit or delete a movie, or fetch a movie that has just been added), though that probably isn't of much interest to the user. The user would be more interested to search by title, case number, or perhaps case and page number or case and title. We'll implement a search feature that accounts for all of these, following these steps:

  • Implement a function in the web interface to handle the processing of form data containing the search criteria
  • Implement a Diet template that displays an input form where the user can enter search criteria
  • Modify index.dt to support the listing of search results
  • Modify app.d to map the input form's URL to the new Diet template
  • Modify layout.dt with a link to the new input form
  • Implement support for multiple search criteria in the database layer

Notice that we're modifying the database layer last this time. This is because db.d is going to require a lot of new code, which we will implement a chunk at a time. By completing the other steps first, we can test each chunk as we add it.

Implementing the postFind function

The postFind function will receive three parameters from the input form: a movie title, a case number, and a page number. A page number can never be a search key by itself, but other than that any combination of the three form fields can be filled out to search for a movie. If the user only enters a title, the search is by title; if case number and page number are filled out, then the search is by case number and page number. The job of postFind is to figure out which fields have values and call findMovie in the database layer with the appropriate flag. So, for the first step, let's add the following enumeration to the top of db.d:

enum Find {
  none        = 0x0,
  byTitle     = 0x1,
  byCase      = 0x2,
  byTitleCase = 0x3,
  byCasePage  = 0x4,
  byAll       = byTitle | byCase | byCasePage,
}

One of Find's members will be the first argument passed to findMovie. Now let's turn to web.d and add postFind to MovieMan.

The first line initializes a local variable, params. After that, the function tests each parameter to determine which of the Find flags apply. If a title was provided, Find.byTitle is set. If there is a case number but no page number, Find.byCase is set, otherwise if there is both a case number and a page number, Find.byCasePage is set:

void postFind(string title, int caseNumber, int pageNumber) {
  Find params;
  if(title !is null && title != "N/A")
    params |= Find.byTitle;
  if(caseNumber && !pageNumber) params |= Find.byCase;
  else if(caseNumber && pageNumber) params |= Find.byCasePage;
    
  auto info = ListInfo(ListType.findMovie);
  info.movies = findMovie(params, title, caseNumber, pageNumber);
  render!("index.dt", info);
}

These few lines cover all the cases we are interested in. Finally, if params is still equal to the default value after the checks, then the index method is called to render the index page with the default parameters. Otherwise, findMovie is called to perform the search and the resulting array is stored in a ListInfo instance, which is then handed off to the render function to list all of the movies on the index page. Notice that info.type is set to ListType.findMovie.

Implementing find.dt

The diet template find.dt looks very much like the add.dt we implemented earlier. The only difference is that it has three input fields and posts its data to /find. Once again, here's a screenshot of the code:

Implementing find.dt

Modifying index.dt

We only need to do a little work in index.dt. First, add two lines to check for ListType.findMovie and display a context-specific header:

-if(info.type == ListType.addMovie)
  h3 Sucessfully Added Movie
-else if(info.type == ListType.findMovie)
  h3 The Following Movies Were Found
-else
  h3 MovieMan Database

Next, we want to also display a context-specific message if the movie array is empty. The default message looks like this already:

-if
  p There are no movies in the database.

Let's change it to this:

-if(info.movies.length == 0 && info.type == ListType.findMovie)
  p No movies match your search criteria.
-else if(info.movies.length == 0)
  p There are no movies in the database.

Modifying app.d and layout.dt

Let's kill two birds with one stone. First, a new line in app.d:

router.get("/forms/add", staticTemplate!"add.dt");
router.get("/forms/find", staticTemplate!"find.dt");
router.get("*", serveStaticFiles("./public/"));

And second, the addition of a link to the form in layout.dt:

ul
  li
    a(href='/') Home
  li 
    a(href='/forms/add') Add New Movie
  li
    a(href='/forms/find') Find Movie(s)

Now we're ready to try out all the pieces we'll add to db.d as we add them.

Implementing the findMovie functions

We're going to implement two versions of the findMovie function. One simply takes an ID to search for; the other takes four parameters, one to indicate which of the other three to use as search criteria. Since the former is shorter and easier to implement, that's where we'll begin.

findMovie the first

Here's the SQL that needs to be added to sql.d:

enum byIDSQL = "SELECT * FROM movie WHERE movieID=?1";

In the interests of saving space, I'll trust that you know by now where and how to add and initialize a Statement named _findByID in db.d. Once you've done that, you can add the following version of findMovie:

Movie[] findMovie(long id) {
  _findByID.reset();
  _findByID.bind(1, id);
  return _findByID.getResults();
}

Even though we're only looking for a single movie here, we're still returning an array. Given that everything else deals with an array of Movies, it fits right in. Now let's go back to web.d and modify postAdd as shown by the highlighted line:

void postAdd(string title, int caseNumber, int pageNumber, int discNumber, int seasonNumber)
{
  auto id = addMovie(Movie(0, title, caseNumber, pageNumber, discNumber, seasonNumber));
  auto info = ListInfo(-1, ListType.addMovie);
  info.movies = findMovie(id);
  render!("index.dt", info);
}

findMovie the second

The second function for db.d is a little long to show all at once. So what we'll do is look at the basic skeleton first, then we'll implement it one case at a time in the switch statement:

Movie[] findMovie(Find by, string title, int caseNumber, int pageNumber) 
{
  Statement sql;
  scope(exit) sql.reset();
  final switch(by) with(Find) {
    case byTitle:
      break;
    case byCase:
      break;
    case byTitleCase:
      break;
    case byCasePage:
      break;
    case byAll:
      break;
    case none:
      return [];
  }
  return sql.getResults;
}

Since we're going to have multiple prepared statements for the different search criteria, we start off by declaring a Statement instance to which we will assign the Statement we need in the switch. For convenience, we'll call reset on it when the function exits. We're using a final switch to guarantee we cover every member of Find, and a with statement to save some keystrokes. Finally, we call getResults on the instance we've assigned to sql.

Each case in the switch will first assign the appropriate Statement to sql, bind the appropriate parameters, then break. The tedious bit is setting up the prepared statements. Again, I'll show you the SQL each statement requires and give you a variable name, then you can add the necessary lines to db.d for each Statement. The first one we'll need is _selectTitleStmt, the SQL for which is:

enum byTitleSQL= "SELECT * FROM movie WHERE title=?1 ORDER BY caseNum, pageNum;";

Following is the case statement to search by title:

case byTitle:
  sql = _findByTitle;
  sql.bind(1, title);
  break;

At this point, you can compile and launch, then click on the Find Movie(s) link to test out searching by title. Next up, we'll need a statement called _findByCase, which requires this SQL:

enum byCaseSQL = "SELECT * FROM movie WHERE caseNum=?1 ORDER BY pageNum";

And the case statement:

case byCase:
  sql = _findByCase;
  sql.bind(1, caseNumber);
  break;

Run DUB again and perform a couple of searches based on case number. Next in line are the _findByTitleCase and the following SQL:

enum byTitleCaseSQL= "SELECT * FROM movie WHERE title=?1 AND caseNum=?2 ORDER BY caseNum, pageNum;";

The implementation:

case byTitleCase:
  sql = _findByTitleCase;
  sql.bind(1, title);
  sql.bind(2, caseNumber);
  break;

There are only two case statements left. Once you've tried searching by title and case number, go ahead and get them both implemented. We'll need two statements, _findByCasePage and _findByAll. The SQL for each:

enum byCasePageSQL = "SELECT * FROM movie WHERE caseNum=?1 AND pageNum=?2 ORDER BY pageNum;";
enum byAllSQL = "SELECT * FROM movie WHERE title=?1 AND caseNum=?2 AND pageNum=?3";

And the code:

case byCasePage:
  sql = _findByCasePage;
  sql.bind(1, caseNumber);
  sql.bind(2, pageNumber);
  break;
case byAll:
  sql = _findByAll;
  sql.bind(1, title);
  sql.bind(2, caseNumber);
  sql.bind(3, pageNumber);
  break;

Editing and deleting movies

In order for this book version of MovieManWeb to become feature complete, it needs to have support for editing and deleting movies. The links to the appropriate URLs are already configured in index.dt and are displayed alongside each movie in the table. We're not going to implement them together, however. Should you choose to do so, adding support for both features is a good first target for expanding on the program. I will outline the necessary steps and show the required SQL here (it is also present in sql.d in the downloadable source package), but other than that, you're on your own.

For edit functionality, you'll need to complete the following steps:

  • Implement a function, updateMovie, in db.d to update the fields of a database entry, given a Movie instance.
  • Implement a function, postEdit, in web.d to take the data from a form, construct a Movie instance, and call the updateMovie function.
  • Implement a function, getEdit, in web.d to call findMovie with an ID, then pass call the render function with edit.dt and the result of the find as template arguments.
  • Implement a Diet template, edit.dt, which contains an input form with fields corresponding to each member of a Movie instance. The default values of each field should be taken from the Movie instance that was given to the render function.
  • Modify index.dt to show a context-specific header after the update.

Notice that using URLRouter to map edit.dt to a URL such as /forms/edit isn't going to work this time. In index.dt, the link to edit a movie looks like this:

a(href='/edit?movieID=!{movie.id}') Edit

The ID of the movie to be edited is added to the URL as a movieID parameter, which means we need to implement a getEdit function that takes a single parameter named movieID. The server will make sure that GET requests made when clicking on the link go to getEdit, and POST requests made from the form in edit.dt go to postEdit (assuming you properly configure the form properties). The SQL for the edit feature is:

enum updateSQL = `UPDATE movie
SET title = ?1, caseNum=?2, pageNum=?3, discNum=?4, seasonNum=?5
WHERE movieID=?6;`;

To support deleting a movie from the database, follow these steps:

  • Implement a function, deleteMovie, in db.d to delete a movie from the database
  • Implement a function, getDelete, in web.d to call deleteMovie

That's all that's needed to delete a movie. How you handle the implementation is entirely up to you, but getDelete must take a movieID parameter to match up with the Delete link in layout.dt. You might first call findMovie with the ID and, if it doesn't exist, render index.dt with an empty array. Otherwise, call deleteMovie and then use the result of findMovie to display the data that was deleted. Don't forget to make use of ListType in both cases.

Expanding on MovieManWeb

Once you've implemented the edit and delete features, there are a number of ways to expand and enhance the program. Perhaps two of the most important are data validation and error handling. To see why, try entering text in a number field, or leaving it empty. What you'll end up with is a page showing you the backtrace of an exception because std.conv.to couldn't convert the text to a number.

The cost of the convenience of the web interface API is that you lose all of the low-level control over data conversion and error handling. There is an attribute with which you can annotate any function in MovieMan, @errorDisplay, which can be given a function that will be called when an exception is thrown during execution of the annotated function (see http://vibed.org/api/vibe.web.web/errorDisplay), but if you want any control over form input validation, you'll need to do it client side with JavaScript. Alternatively, you could implement a new version of the program using the low-level API to get a feel for how it works. That would also give you complete control over how to handle invalid parameters and form data.

vibe.d has support for localization. Adding support to display the UI in multiple languages would be a nice little project to work on. Other ideas include expanding the database to allow for movie directors, actors and actresses, producers, release dates, notes, or any other data you'd like to support. This sort of project would touch every aspect of the code base, requiring more SQL statements, more D code and more Diet templates. You could also add support for music CDs.

If you want to take MovieManWeb (or anything you derive from it) online, you'll need to take steps for security first. At a minimum you'll want to add support for user accounts and input sanitization. Rather than worrying about storing salted and hashed passwords and user IDs, it might be a better idea to use something such as Google Sign-in (https://developers.google.com/identity/sign-in/web/sign-in) or other APIs to allow logging in with using credentials from popular social media websites.

Whatever you decide to do with the code presented here, have fun. The D programming language is always a pleasure to use, but using it together with vibe.d is quite sublime.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.186.167