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:
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:
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.
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:
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.
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:
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.
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:
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:
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
.
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:
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.
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.
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:
@property
that are getters (no parameters, return a value) are mapped to GET requests@property
that are setters (one parameter, no return value) are mapped to PUT requestsget
or query
are mapped to GET requestsset
or put
are mapped to PUT requestsadd
, create
, post
, or no prefix at all are mapped to POST requestsremove
, erase
, or delete
are mapped to DELETE requestsupdate
or patch
are mapped to PATCH requestsBy 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.
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.
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;
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:
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.
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:
db.d
to add a movie to the database.web.d
that can map to a POST request handling data from an input form.index.dt
to display the new movie.app.d
to handle requests for the input form's URL.layout.dt
.Let's get to work.
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.
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.
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 Movie
s 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); }
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:
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.
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.
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.
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.
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:
index
function to fetch movies from the database and pass them off to the render
functionindex.dt
to display Next and Previous links when appropriateThe 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 Movie
s. 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 Row
s. 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.
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.
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); }
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:
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.
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:
index.dt
to support the listing of search resultsapp.d
to map the input form's URL to the new Diet templatelayout.dt
with a link to the new input formNotice 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.
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
.
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:
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.
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.
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.
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 Movie
s, 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);
}
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;
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:
updateMovie
, in db.d
to update the fields of a database entry, given a Movie
instance.postEdit
, in web.d
to take the data from a form, construct a Movie
instance, and call the updateMovie
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.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.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:
deleteMovie
, in db.d
to delete a movie from the databasegetDelete
, 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.
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.
3.142.12.207