Build Web Apps in Vibe
Build Web Apps in Vibe
Rey Valeza
Introduction
This is a book written by a learner for fellow learners of Vibe.d. I am no expert on D, but
I am happy to note that an expert-level knowledge of D is not required to develop apps in
Vibe.d.
I hankered for Vibe.d and its simplicity after looking at the complexity of Spring /
Hibernate and the technologies needed just to build a simple application. Yes, Java is
fantastic. Fantastically complex. I long for the simpler days of Java up to JDK 1.4 with
just JSP’s, servlets and JDBC.
I built a web app with Vibe.d a few years back. I found there aren’t any drastic changes
since then, and I’m glad. That is how it should be. That is why PHP is still so popular:
simplicity and no drastic changes. Young people can pick up any old book or tutorial and
find it isn’t much different from the current PHP.
Why did Rails fail to gain traction? Because by the time it was attracting droves of
potential converts, they changed drastically by adopting the then hot-off-the-oven REST,
ignoring the users who were already happy with the previous ways and frustrating new
converts desperately looking for updated tutorials and documentation.
Why did Meteor fail too? Because by the time it was catching people’s curiosity, they
decided to adopt the very popular React. For existing users, the quandary was to go the
old Blaze way or the React way. Which way will win eventually? Since there was no way
of divining the future, new and old users alike took the third way: abandon ship.
You do not make drastic changes to your already revolutionary product when you haven’t
achieved a critical mass of users yet. You will not expand user base by constantly
adopting each and every sizzling-new buzzword, you only end up losing them.
Remember how Java did it? There was just so much hype at the beginning. There was
just so much buzz. Java was everywhere. Everybody was curious. Converts came in
droves. And Sun backed it all up with solid, updated documentation. That’s how you do
it. The Sun people were pros compared to these new upstarts. So even if Java kept
changing, it has already achieved a critical mass of followers long ago so that users have
no choice but to follow changes as a captive audience. Java slew the lumbering dinosaurs
then. Sadly, Java is now the dinosaur.
The Vibe.d framework was created by Sönke Ludwig out of frustration with existing
frameworks, notably Node.js. If you are intrigued by that, find out more at
https://vibed.org/. You can find the forum at https://forum.rejectedsoftware.com/.
Vibe.d doesn’t rely on hype, which plays against it. I feel there is a dearth of tutorials on
Vibe.d. After opting to learn Vibe.d again, I found I forgot so much, so I decided to write
this book while I am re-learning it.
Configuring your Windows system for Vibe.d
I am using Windows 10 so I hope your system mimics mine, but you should be fine with
another OS you are familiar with, and I assume you know the equivalent Windows
commands in your system’s shell terminal. We will be using the terminal or command
window a lot. I often find that Linux users easily follow Windows-based tutorials but not
the other way around. Which means most Linux users are or were Windows users too.
I did the projects in an Ubuntu system before writing this book, so I rewrote the projects
in Windows 10 as I know most developers use or prefer Windows.
D is a language that compiles to native code. The Vibe.d framework is essentially a set of
libraries written in D, which means your code needs to be linked to those libraries and
dependencies and compiled into a native executable. To develop applications in Vibe.d,
we need at least a D compiler and a text editor.
Regarding compilers, there are three choices: DMD, GDC and LDC. They tell us that for
development purposes, we should use the DMD compiler for its speed of compilation.
When we are ready to deploy the final production version, then we should use one of the
other compilers (GDC or LDC) for the runtime speed and optimizations of the binary
output. This book uses the DMD compiler. I hope you do too.
Go ahead and open a terminal or command window. We will be using that window a lot.
To test the installation of the DMD compiler, type dmd on the terminal window. If the
installation is successful, it will fill the window with help information.
C:\DCompiler>dmd
DMD64 D Compiler v2.094.2-dirty
Copyright (C) 1999-2020 by The D Language Foundation, All Rights Reserved written by
Walter Bright
Documentation: https://dlang.org/
Config file: C:\DCompiler\dmd2\windows\bin64\sc.ini
Usage:
dmd [<option>...] <file>...
dmd [<option>...] -run <file> [<arg>...]
Where:
<file> D source file
<arg> Argument to pass when running the resulting program
<option>:
@<cmdfile> read arguments from cmdfile
-allinst generate code for all template instantiations
-betterC omit generating some runtime information and helper functions
-boundscheck=[on|safeonly|off]
bounds checks on, in @safe only, or off
-c compile only, do not link
-check=[assert|bounds|in|invariant|out|switch][=[on|off]]
enable or disable specific checks
-check=[h|help|?] list information on all available checks
-checkaction=[D|C|halt|context]
behavior on assert/boundscheck/finalswitch failure
-checkaction=[h|help|?]
list information on all available check actions
-color turn colored console output on
-color=[on|off|auto]
force colored console output on or off, or only when not redirected (default)
-conf=<filename> use config file at filename
-cov do code coverage analysis
-cov=ctfe Include code executed during CTFE in coverage report
-cov=<nnn> require at least nnn% code coverage
-D generate documentation
-Dd<directory> write documentation file to directory
-Df<filename> write documentation file to filename
-d silently allow deprecated features and symbols
-de issue an error when deprecated features or symbols are used (halt
compilation)
-dw issue a message when deprecated features or symbols are used (default)
-debug compile in debug code
-debug=<level> compile in debug code <= level
-debug=<ident> compile in debug code identified by ident
-debuglib=<name> set symbolic debug library to name
-defaultlib=<name>
set default library to name
-deps print module dependencies (imports/file/version/debug/lib)
-deps=<filename> write module dependencies to filename (only imports)
-extern-std=<standard>
set C++ name mangling compatibility with <standard>
-extern-std=[h|help|?]
list all supported standards
-g add symbolic debug info
-gf emit debug info for all referenced types
-gs always emit stack frame
-gx add stack stomp code
-H generate 'header' file
-Hd=<directory> write 'header' file to directory
-Hf=<filename> write 'header' file to filename
-HC[=[silent|verbose]]
generate C++ 'header' file
-HC=[?|h|help] list available modes for C++ 'header' file generation
-HCd=<directory> write C++ 'header' file to directory
-HCf=<filename> write C++ 'header' file to filename
--help print help and exit
-I=<directory> look for imports also in directory
-i[=<pattern>] include imported modules in the compilation
-ignore ignore unsupported pragmas
-inline do function inlining
-J=<directory> look for string imports also in directory
-L=<linkerflag> pass linkerflag to link
-lib generate library rather than object files
-lowmem enable garbage collection for the compiler
-m32 generate 32 bit code
-m32mscoff generate 32 bit code and write MS-COFF object files
-m64 generate 64 bit code
-main add default main() (e.g. for unittesting)
-man open web browser on manual page
-map generate linker .map file
-mcpu=<id> generate instructions for architecture identified by 'id'
-mcpu=[h|help|?] list all architecture options
-mixin=<filename> expand and save mixins to file specified by <filename>
-mscrtlib=<libname>
MS C runtime library to reference from main/WinMain/DllMain
-mv=<package.module>=<filespec>
use <filespec> as source file for <package.module>
-noboundscheck no array bounds checking (deprecated, use -boundscheck=off)
-O optimize
-o- do not write object file
-od=<directory> write object & library files to directory
-of=<filename> name output file to filename
-op preserve source path for output files
-preview=<name> enable an upcoming language change identified by 'name'
-preview=[h|help|?]
list all upcoming language changes
-profile profile runtime performance of generated code
-profile=gc profile runtime allocations
-release compile release version
-revert=<name> revert language change identified by 'name'
-revert=[h|help|?]
list all revertable language changes
-run <srcfile> compile, link, and run the program srcfile
-shared generate shared library (DLL)
-transition=<name>
help with language change identified by 'name'
-transition=[h|help|?]
list all language changes
-unittest compile in unit tests
-v verbose
-vcolumns print character (column) numbers in diagnostics
-verror-style=[digitalmars|gnu]
set the style for file/line number annotations on compiler messages
-verrors=<num> limit the number of error messages (0 means unlimited)
-verrors=context show error messages with the context of the erroring source line
-verrors=spec show errors from speculative compiles such as __traits(compiles,...)
--version print compiler version and exit
-version=<level> compile in version code >= level
-version=<ident> compile in version code identified by ident
-vgc list all gc allocations including hidden ones
-vtls list all variables going into thread local storage
-vtemplates=[list-instances]
list statistics on template instantiations
-w warnings as errors (compilation will halt)
-wi warnings as messages (compilation will continue)
-X generate JSON file
-Xf=<filename> write JSON file to filename
C:\DCompiler>
dmd --version will show you just the DMD compiler version.
The DMD installer comes with the dub package manager. It is the default package
manager for D projects and was created by the author of the Vibe.d framework himself,
Sönke Ludwig.
dub
dub help
or
dub -h
or
dub --help
dub --version
After the compiler is installed, the next thing we need is a text editor or an IDE. You can
use your favorite IDE/text editor while following this book, but this book will use the
ubiquitous and free Visual Studio Code. Since you are using Windows, you must have
that already. To those who haven’t yet, head to
https://code.visualstudio.com/download
Simply double-click on the downloaded file and follow the instructions on the setup
wizard.
After installation, add the D language extension so VS Code can recognize D language
code.
Add the D language utility extension pack. With this pack, you will have features like
syntax color highlighting, code-completion and some error flagging.
or
PS C:\vibeprojects> cd hello
PS C:\vibeprojects\hello> dir
Directory: C:\vibeprojects\hello
PS C:\vibeprojects\hello>
You are now inside the root directory of the hello application. To compile and run this
application in one step (you haven’t done anything yet!), type
dub
Since this is the first time you run this command, this will download all the dependencies,
then compile, then link, then finally run the application.
PS C:\vibeprojects\hello> dub
Fetching vibe-core 1.11.1 (getting selected version)...
Fetching botan-math 1.0.3 (getting selected version)...
Fetching taggedalgebraic 0.11.18 (getting selected version)...
Fetching vibe-d 0.9.2 (getting selected version)...
Fetching memutils 1.0.4 (getting selected version)...
Fetching stdx-allocator 2.77.5 (getting selected version)...
Fetching botan 1.12.18 (getting selected version)...
Fetching diet-ng 1.7.4 (getting selected version)...
Fetching openssl 1.1.6+1.0.1g (getting selected version)...
Fetching eventcore 0.9.11 (getting selected version)...
Fetching mir-linux-kernel 1.0.1 (getting selected version)...
Fetching libasync 0.8.6 (getting selected version)...
Performing "debug" build using C:\DCompiler\dmd2\windows\bin\dmd.exe for x86_64.
taggedalgebraic 0.11.18: building configuration "library"...
eventcore 0.9.11: building configuration "winapi"...
stdx-allocator 2.77.5: building configuration "library"...
vibe-core 1.11.1: building configuration "winapi"...
vibe-d:utils 0.9.2: building configuration "library"...
vibe-d:data 0.9.2: building configuration "library"...
mir-linux-kernel 1.0.1: building configuration "library"...
vibe-d:crypto 0.9.2: building configuration "library"...
diet-ng 1.7.4: building configuration "library"...
vibe-d:stream 0.9.2: building configuration "library"...
vibe-d:textfilter 0.9.2: building configuration "library"...
vibe-d:inet 0.9.2: building configuration "library"...
vibe-d:tls 0.9.2: building configuration "openssl-mscoff"...
vibe-d:http 0.9.2: building configuration "library"...
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\range\primitives.d(175,38):
Deprecation: alias byKeyValue this is deprecated - Iterate over .byKeyValue instead.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\range\primitives.d(177,27):
Deprecation: alias byKeyValue this is deprecated - Iterate over .byKeyValue instead.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\range\primitives.d(175,38):
Deprecation: alias byKeyValue this is deprecated - Iterate over .byKeyValue instead.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\range\primitives.d(177,27):
Deprecation: alias byKeyValue this is deprecated - Iterate over .byKeyValue instead.
vibe-d:mail 0.9.2: building configuration "library"...
vibe-d:mongodb 0.9.2: building configuration "library"...
C:\DCompiler\dmd2\windows\bin\..\..\src\druntime\import\core\internal\traits.d(341,61
): Deprecation: function std.typecons.Nullable!int.Nullable.get_ is deprecated - Implicit
conversion with alias Nullable.get this will be removed after 2.096. Please use .get
explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!string.Nullable.get_ is deprecated - Implicit conversion
with alias Nullable.get this will be removed after 2.096. Please use .get explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!(Alternate).Nullable.get_ is deprecated - Implicit
conversion with alias Nullable.get this will be removed after 2.096. Please use .get
explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!(MaxVariable).Nullable.get_ is deprecated - Implicit
conversion with alias Nullable.get this will be removed after 2.096. Please use .get
explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!string.Nullable.get_ is deprecated - Implicit conversion
with alias Nullable.get this will be removed after 2.096. Please use .get explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!(Alternate).Nullable.get_ is deprecated - Implicit
conversion with alias Nullable.get this will be removed after 2.096. Please use .get
explicitly.
C:\DCompiler\dmd2\windows\bin\..\..\src\phobos\std\format.d(3648,26): Deprecation:
function std.typecons.Nullable!(MaxVariable).Nullable.get_ is deprecated - Implicit
conversion with alias Nullable.get this will be removed after 2.096. Please use .get
explicitly.
vibe-d:redis 0.9.2: building configuration "library"...
vibe-d:web 0.9.2: building configuration "library"...
vibe-d 0.9.2: building configuration "vibe-core"...
hello ~master: building configuration "application"...
Linking...
Copying files for vibe-d:tls...
Running .\hello.exe
[main(----) INF] Listening for requests on http://[::1]:8080/
[main(----) INF] Listening for requests on http://127.0.0.1:8080/
[main(----) INF] Please open http://127.0.0.1:8080/ in your browser.
The last three lines indicate that the project has successfully compiled and your server is
now running.
http://127.0.0.1:8080
or
http://localhost:8080
and you will see the ‘Hello, World!’ message in your browser.
To stop the running application, press Ctrl-C (Control-C) on the terminal window.
dub build - completely compile the whole project and its dependencies
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, &hello);
All console applications in D start with a main() function. It is the entry point of the
application.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
The line
import vibe.vibe;
means import the file (or module) vibe\vibe.d. The convention in D is to use the file name
as the module name so keeping track of namespaces and files are simpler. The vibe.vibe
library module contains almost all of the Vibe.d API so it is very convenient for users.
The import directive is equivalent to C and C++’s #include preprocessor directive, but C
and C++ insert the whole file into the importing file while D does not.
void main()
The return value of main() can either be void or int, which is the exit status. The main()
function can also take an array of strings, but we don’t use them in Vibe.d.
The term auto means let the compiler deduce the datatype from the context, so it’s
another convenience to us from D. Writing auto is a lot simpler that writing
HTTPServerSettings.
^C
c:\vibeprojects\hello>^D[00000000(----) INF] Received signal 2. Shutting down.
Warning: 4 socket handles leaked at driver shutdown.
Warning: 4 socket handles leaked at driver shutdown.
In the Explorer window of VS Code, create a new HTML file inside the views\ folder and
name it helloagain.html.
<html>
<head>
<title>Hello again!</title>
</head>
<body>
<h2>Hello again, World!</h2>
</body>
</html>
Vibe.d is a stickler for proper indentation of HTML tags, so follow proper indentation.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, staticTemplate!"helloagain.html");
And delete the hello() function because it is no longer called. Compile and run the project
with dub.
c:\vibeprojects\hello>dub
Performing "debug" build using C:\DCompiler\dmd2\windows\bin\dmd.exe for x86_64.
taggedalgebraic 0.11.18: target for configuration "library" is up to date.
eventcore 0.9.11: target for configuration "winapi" is up to date.
stdx-allocator 2.77.5: target for configuration "library" is up to date.
vibe-core 1.11.1: target for configuration "winapi" is up to date.
vibe-d:utils 0.9.2: target for configuration "library" is up to date.
vibe-d:data 0.9.2: target for configuration "library" is up to date.
mir-linux-kernel 1.0.1: target for configuration "library" is up to date.
vibe-d:crypto 0.9.2: target for configuration "library" is up to date.
diet-ng 1.7.4: target for configuration "library" is up to date.
vibe-d:stream 0.9.2: target for configuration "library" is up to date.
vibe-d:textfilter 0.9.2: target for configuration "library" is up to date.
vibe-d:inet 0.9.2: target for configuration "library" is up to date.
vibe-d:tls 0.9.2: target for configuration "openssl-mscoff" is up to date.
vibe-d:http 0.9.2: target for configuration "library" is up to date.
vibe-d:mail 0.9.2: target for configuration "library" is up to date.
vibe-d:mongodb 0.9.2: target for configuration "library" is up to date.
vibe-d:redis 0.9.2: target for configuration "library" is up to date.
vibe-d:web 0.9.2: target for configuration "library" is up to date.
vibe-d 0.9.2: target for configuration "vibe-core" is up to date.
helloagain ~master: building configuration "application"...
Compiling Diet HTML template helloagain.html...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
Copying files for vibe-d:tls...
Running .\hello.exe
[main(----) INF] Listening for requests on http://[::1]:8080/
[main(----) INF] Listening for requests on http://127.0.0.1:8080/
[main(----) INF] Please open http://127.0.0.1:8080/ in your browser.
You see that the required libraries were no longer downloaded so the compilation is a bit
faster. Refresh your browser and you should see the new message.
To stop the running app, press Ctrl-C. Twice if needed.
As you might have noticed, this is the conventional directory layout of a Vibe.d app using
dub:
Edit views\helloagain.html
<!DOCTYPE html>
<html>
<head>
<title>My webpage!</title>
<link rel="stylesheet" href="css/helloagain.css" />
<script async src="js/helloagain.js"></script>
</head>
<body>
<h1>Hello, World!</h1>
<h4 id='date'></h4>
<div class="image-section">
<div class="section-style">
<img src="https://source.unsplash.com/random/400x200" alt="" />
<p>A random image courtesy of unsplash.com.</p>
</div>
<div class="section-style">
<img src="https://source.unsplash.com/random/400x200" alt="" />
<p>A random image courtesy of unsplash.com.</p>
</div>
</div>
<div class="image-section">
<div class="section-style">
<img src="https://source.unsplash.com/random/400x200" alt="" />
<p>A random image courtesy of unsplash.com.</p>
</div>
<div class="section-style">
<img src="https://source.unsplash.com/random/400x200" alt="" />
<p>A random image courtesy of unsplash.com.</p>
</div>
</div>
</body>
</html>
Actually, I got this from the wild wild web courtesy of Programming Liftoff
(https://dev.to/programliftoff/create-a-basic-webpage-with-css-and-javascript--104i)
Next, create a css\ folder inside the public\ folder (you can right-click on it)
Then create the public\css\helloagain.css stylesheet file (also from Programming Liftoff)
inside that new folder.
body {
text-align: center;
background-color: #f0e8c5;
}
div {
margin-top: 15px;
}
.image-section {
display: flex;
justify-content: center;
}
.section-style {
margin-right: 25px;
margin-left: 25px;
background-color: white;
}
Next, create the \js folder, also inside the public\ folder.
Then create the public\js\helloagain.js with the following contents (also from
Programming Liftoff)
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
The line
router.get("/", staticTemplate!"helloagain.html");
means to display helloagain.html when the web browser URL shows “/”, meaning the
helloagain.html is the index page or home page. The “/” URL expands to
http://localhost:8080/, which is the root of the application.
The line
router.get("*", serveStaticFiles("public/"));
means that for all other files not indicated by any route, look for them inside the public/
folder.
We indicated in the helloagain.html page that the stylesheet and the javascript files are in
/public/css/ and /public/js/ but without specifically naming the public/ folder.
After refreshing the browser, something similar is what you will see:
The images you see will most likely be different as they are randomly selected every time
you refresh the page.
Creating templates with Diet
Vibe.d comes with a templating engine called Diet.
<!DOCTYPE html>
<html>
<head>
<title>Simple hello!</title>
</head>
<body>
<h1>Simple hello, World!</h1>
<h2>How are you doing today?</h2>
</body>
</html>
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, staticTemplate! "simplehello.html");
Then, in your terminal, build and run the project and refresh the browser.
We just tested that the new HTML file is working. Now, let’s convert the HTML file into
a Diet template file.
First, save the views\simplehello.html file as views\simplehello.dt and edit
views\simplehello.dt to make it look like this:
doctype html
html
head
title Simple hello using a template!
body
h1 Simple hello world from a template!
h2 How are you doing today?
| Please visit the home of <a href="https://vibed.org/">Vibe.d</a>
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, staticTemplate! "simplehello.dt");
In your terminal, stop the app if it is still running (Ctrl-C), then compile and run.
dub --force
We sometimes use the --force option to tell dub to recompile everything again, just in
case it fails to see that some files were changed and needed to be recompiled or that there
are updated libraries that needed to be downloaded.
| (pipe) tag at the start of a line means the line is composed of plain text.
:javascript tag at the start of a line means JavaScript code on the following indented lines.
:javascript
function greet()
{
var greeting = "Hello, world!";
alert(greeting);
}
:css tag at the start of a line means CSS code on the following indented lines.
:css
.alert-message
{
font-weight: bold;
color: brown;
text-align: center;
}
div(class="high-status", id="high-status-1");
A tag followed by a . (dot) followed by an identifier name means a CSS class name.
div.high-status
div#high-status-1
div.high-status.row-major.row-inner
div.
These lines are all simple text
but may contain HTML code
like <a href="/">this</a>.
<!DOCTYPE html>
<html>
<head>
<title>Demo site</title>
</head>
<body>
<header>
<h2>Welcome to our not so simple site</h2>
</header>
<nav>
<ul>
<li><a href="about.html">About us</a></li>
<li><a href="events.html">Events</a></li>
<li><a href="contact.html">Contact us</a></li>
</ul>
</nav>
<article>
<hl>Welcome to our not so simple site!</hl>
<div>
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book.
</div>
</article>
<footer>
<p>Copyright 2021 Notsosimple Company</p>
</footer>
</body>
</html>
Feel free to write your own text here as the Lorem ipsum text is just filler.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, staticTemplate! "notsosimplehello.html");
Now that we have tested that the HTML page is working, let’s convert it into a Diet
template. Save notsosimplehello.html as notsosimplehello.dt and convert it to a template.
html
head
title Demo site
body
header
h2 Welcome to our not so simple site
nav
ul
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
article
hl Welcome to our not so simple site!
div.
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book.
footer
p Copyright 2021 Notsosimple Company
Take note of the dot (.) after the div. It means all the indented lines that follow are plain
text (which could include HTML).
It feels strange at first but you realize there is less to type when you use Diet templates.
Copy notsosimplehello.dt into about.dt, events.dt and contact.dt inside the views folder
(or File->Save As… three times)
Make minor changes to the new files so they each display a different message.
views\about.dt
html
head
title Demo site
body
header
h2 This is the About Us page
nav
ul
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
article
div.
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book.
footer
p Copyright 2021 Notsosimple Company
views\contact.dt
html
head
title Demo site
body
header
h2 This is the Contact Us page
nav
ul
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
article
div.
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book.
footer
p Copyright 2021 Notsosimple Company
views\events.dt
html
head
title Demo site
body
header
h2 This is the Events page
nav
ul
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
article
div.
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book.
footer
p Copyright 2021 Notsosimple Company
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, staticTemplate! "notsosimplehello.dt");
logInfo("Please open http://127.0.0.1:8080/ in your browser.");
runApplication();
}
Then compile, run and refresh your browser. If you see no changes, that means you did
good.
But when you click on the links, it is still showing the same page although the URL on
the browser address bar changes
That’s because the server thinks we are just going to display the same page,
notsosimplehello.dt, no matter the URL.
We need a router to tell the server to display a certain page when given a certain URL.
This matching of URLs to pages or actions is called routing.
Edit source\app.d to make use of a router to display the appropriate page when given a
specified URL.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
In D, we can make use of the auto keyword if the data type can be deduced. The line
means router is a URLRouter class type. We could have also written this as
The line
http://localhost:8080/
The line
http://localhost:8080/about
And so on.
Compile, run and refresh your browser. Now the links should work.
Making use of your own functions
You can also make calls to your own functions. Add the following link to
notsosimplehello.dt.
html
head
title Demo site
body
header
h2 Welcome to our not so simple site
nav
ul
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
li <a href="/helloagain">Hello again!</a>
article
h1 Welcome to our not so simple site!
div Lorem Ipsum is simply dummy text of the printing industry.
footer
p Copyright 2021 Notsosimple Company
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
void helloAgain(HTTPServerRequest req, HTTPServerResponse res)
{
string title = "Hello, World!";
res.writeBody(title);
}
Compile, run, refresh your browser, then click on the Hello again! link and you will see
that the new function gets called.
C:\vibeprojects\hello>cd ..
C:\vibeprojects>cd helloblocks
C:\vibeprojects\helloblocks>
Create views\mainlayout.dt.
html
head
title Using templates
body
nav
ul
li <a href="/">Home</a>
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
li <a href="/helloagain">Hello again!</a>
block content
footer
p Copyright 2021 Notsosimple Company
The line
block content
means we have named that part, or ‘block’, of the layout as ‘content’. You can give the
block any name as long as you give the same name to the other templates that inherit
from this template. There is nothing inside that block because it will be defined by the
other pages.
Create views\home.dt.
extends mainlayout
block content
article
h1 Welcome to our not so simple site!
The first line indicates that this template inherits from mainlayout.dt, meaning all of
mainlayout.dt are included in this template.
The second line, block content, indicates that the following lines will appear in the
corresponding content block indicated by mainlayout.dt.
Create views\about.dt.
extends mainlayout
block content
article
h1 About us
Create views\events.dt.
extends mainlayout
block content
article
h1 Upcoming events
Create views\contact.dt.
extends mainlayout
block content
article
h1 Contact us
Next, edit source\app.d to make it look like this:
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
By creating a layout template and using the block mechanism, we placed the repeating
tags in one template while leaving the other templates with only their specific contents.
We named the part that changes as the content block and we indicated in the other pages
that their contents will appear in that content block of the layout.
Compile and run the project with dub, then refresh the browser.
Using include in templates
There is also an include directive in Diet templates that come in handy when breaking up
templates into smaller parts.
nav
ul
li <a href="/">Home</a>
li <a href="/about">About us</a>
li <a href="/events">Events</a>
li <a href="/contact">Contact us</a>
li <a href="/helloagain">Hello again!</a>
footer
p Copyright 2021 Notsosimple Company
html
head
title Using templates
body
include navbar
block content
include footer
C:\vibeprojects\helloblocks>dub
Sometimes all you need to do is erase all the indentations and manually indent them
again using tabs. After editing, compile, run and refresh the browser.
A sticky navbar means a navigation bar that always stay on top of the page, no matter the
length and shape of the web page. A sticky footer always stay at the bottom.
Stop the running app with Ctrl-C, then create a new project named mybootstrap.
c:\vibeprojects\hello>cd ..
c:\vibeprojects>cd mybootstrap
c:\vibeprojects\mybootstrap>
To use Bootstrap in our web page, we should have access to the Bootstrap files.
Download the Bootstrap files here: https:// getbootstrap.com/docs/5.0/getting-
started/download/
After extracting, copy the whole css and the js folders and paste them into the public
folder of your project
On the same Bootstrap page, download and extract the Bootstrap examples
After extraction, you will find there are two files inside the sticky-footer-navbar folder
To test and see how it looks on the browser, simply double-click on index.html
Copy the index.html file into the views\ folder of your project. Inside VS Code, open the
c:\vibeprojects\mybootstrap folder.
<!doctype html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors"
>
<meta name="generator" content="Hugo 0.79.0">
<title>Sticky Footer Navbar Template · Bootstrap v5.0</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/sticky-
footer-navbar/">
<link href="css/bootstrap.min.css" rel="stylesheet">
:css
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
<link href="css/sticky-footer-navbar.css" rel="stylesheet">
</head>
<body class="d-flex flex-column h-100">
<header>
<!-- fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Fixed navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-
bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-
label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item active">
<a class="nav-link" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#" tabindex="-1" aria-
disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<!-- Begin page content -->
<main class="flex-shrink-0">
<div class="container">
<h1 class="mt-5">Sticky footer with fixed navbar</h1>
<p class="lead">Pin a footer to the bottom of the viewport in desktop browsers
with this custom HTML and CSS. A fixed navbar has been added with <code class="small">p
adding-top: 60px;</code> on the <code class="small">main > .container</code>.</p>
</div>
</main>
<!-- the footer -->
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<span class="text-muted">Place sticky footer content here.</span>
</div>
</footer>
<script src="js/bootstrap.bundle.min.js"></script>
</body>
</html>
Edit source\app.d.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
The serveStaticFiles(“public/”) function indicates that for other files (or URLs) not
specifically mentioned by the router, look for them under the public/ folder. This way, the
line
will be expanded to
<script src="js/bootstrap.bundle.min.js"></script>
will be expanded to
<script src="public/js/bootstrap.bundle.min.js"></script>
We do not include the public/ part in the path because the server already knows.
Create views\layout.dt.
html(lang="en", class="h-100")
head
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1")
meta(name="description", content="")
meta(name="author", content="Mark Otto, Jacob Thornton and others")
meta(name="generator", content="Hugo 0.79.0")
title Sticky Footer Navbar Template • Bootstrap v5.0
link(href="css/bootstrap.min.css", rel="stylesheet")
:css
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
link(href="css/sticky-footer-navbar.css", rel="stylesheet")
body.d-flex.flex-column.h-100
header
//- fixed navbar
nav.navbar.navbar-expand-md.navbar-dark.fixed-top.bg-dark
div.container-fluid
a.navbar-brand(href="#") Fixed navbar
button.navbar-toggler(type="button", data-bs-toggle="collapse", data-bs-
target="#navbarCollapse", aria-controls="navbarCollapse", aria-
expanded="false", aria-label="Toggle navigation")
span.navbar-toggler-icon
div.collapse.navbar-collapse#navbarCollapse
ul.navbar-nav.me-auto.mb-2.mb-md-0
li.nav-item
a.nav-link.active(aria-current="page", href="#") Home
li.nav-item
a.nav-link(href="#") Link
li.nav-item
a.nav-link.disabled(href="#", tabindex="-1", aria-
disabled="true") Disabled
//- Begin page content
main.flex-shrink-0
div.container
h1.mt-5 Sticky footer with fixed navbar
p.lead.
Pin a footer to the bottom of the viewport in desktop browsers with this
custom HTML and CSS. A fixed navbar has been added
with <code class="small">padding-
top: 60px;</code> on the <code class="small">main > .container</code>.
//- the footer -->
footer.footer.mt-auto.py-3.bg-light
div.container
span.text-muted Place sticky footer content here.
script(src="js/bootstrap.bundle.min.js")
has become
html(lang="en", class="h-100")
html.h-100(lang="en")
The line
has become
Since there are two attributes to the viewport meta tag, namely name and content, we
have to separate them with a comma.
The line
div.collapse.navbar-collapse#navbarCollapse
Since collapse and navbar-collapse are CSS classes applied to the same <div> tag, we
string them together with dots (.), and since navbarCollapse is a CSS ID, we appended it
with a #.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
Compile, run and refresh your browser. If you find no changes, you did good.
Now, time to break up the layout into smaller, manageable and logical chunks for ease of
update.
Breaking up layout.dt into smaller parts with include
Let’s break up views\layout.dt into manageable chunks to make the app easier to edit.
We will extract from views\layout.dt and paste them into different files.
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1")
meta(name="description", content="")
meta(name="author", content="Mark Otto, Jacob Thornton, and Bootstrap contributors")
meta(name="generator", content="Hugo 0.79.0")
title Sticky Footer Navbar Template · Bootstrap v5.0
link(href="css/bootstrap.min.css", rel="stylesheet")
:css
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
link(href="css/sticky-footer-navbar.css", rel="stylesheet")
nav.navbar.navbar-expand-md.navbar-dark.fixed-top.bg-dark
div.container-fluid
a.navbar-brand(href="#") Fixed navbar
button.navbar-toggler(type="button", data-bs-toggle="collapse", data-bs-
target="#navbarCollapse", aria-controls="navbarCollapse", aria-
expanded="false", aria-label="Toggle navigation")
span.navbar-toggler-icon
div.collapse.navbar-collapse#navbarCollapse
ul.navbar-nav.me-auto.mb-2.mb-md-0
li.nav-item
a.nav-link.active(aria-current="page", href="#") Home
li.nav-item
a.nav-link(href="#") Link
li.nav-item
a.nav-link.disabled(href="#", tabindex="-1", aria-
disabled="true") Disabled
div.container
span.text-muted Place sticky footer content here.
script(src="js/bootstrap.bundle.min.js")
extends layout.dt
block maincontent
div.container
h1.mt-5 Sticky footer with fixed navbar
p.lead.
Pin a footer to the bottom of the viewport in desktop browsers
with this custom HTML and CSS. A fixed navbar has been added
with <code class="small">padding-top: 60px;</code> on the
<code class="small">main > .container</code>.
The dot at the end of the p.lead. tag indicates that the next indented lines are plain text.
html(lang="en", class="h-100")
head
include headmeta.dt
body.d-flex.flex-column.h-100
header
include navbar.dt
main.flex-shrink-0
block maincontent
footer.footer.mt-auto.py-3.bg-light
include footer.dt
include bootstrapscript.dt
This time, we named the block with changing content maincontent. You can give the
block any non-keyword name, just like the rules on variable names.
import vibe.vibe;
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
listenHTTP(settings, router);
runApplication();
}
Compile, run and refresh the browser. If it showed the navbar and the footer as it did
before, you did things right.
Layout without Bootstrap: using CSS Grid
How about layout without Bootstrap?
Thankfully, standard CSS has CSS Grid. CSS Grid was designed to make it easier to lay
out components on a page using standard CSS. It divides the page into a grid composed
of rows and columns, like a table, which is similar to Bootstrap’s row- and col- classes.
Create views\cssgrid.html:
<html>
<head>
<title>CSS Grid Sample</title>
<style>
.container
{
margin: 0 auto;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 150px 150px 150px;
grid-template-rows: 200px 200px 200px;
}
.entry
{
color: white;
text-align: center;
display: table-cell;
vertical-align: middle;
}
#first { background-color: red; }
#second { background-color: green; }
#third { background-color: blue; }
#fourth { background-color: teal; }
#fifth { background-color: grey; }
#sixth { background-color: brown; }
#seventh { background-color: silver; }
#eighth { background-color: maroon; }
#ninth { background-color: cyan; }
</style>
</head>
<body>
<div class="container">
<div class="entry" id="first">1st</div>
<div class="entry" id="second">2nd</div>
<div class="entry" id="third">3rd</div>
<div class="entry" id="fourth">4th</div>
<div class="entry" id="fifth">5th</div>
<div class="entry" id="sixth">6th</div>
<div class="entry" id="seventh">7th</div>
<div class="entry" id="eighth">8th</div>
<div class="entry" id="ninth">9th</div>
</div>
</body>
</html>
The line
means we are defining three columns, with each column 150 pixels wide.
The line
means we are declaring three rows, each row with a height of 200 pixels. This way, we
have defined a three by three grid.
Do not compile and run. Simply double-click the file to see how it looks on the browser.
You see that even when you resize the browser, the blocks remain the same sizes.
Now let’s change the CSS to make the center column expandable.
.container
{
margin: 0 auto;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 150px auto 150px;
grid-template-rows: 200px 200px 200px;
}
Refresh the browser. When you expand or contract the browser, the middle column
expands automatically.
Now let’s make the middle row expandable too.
display: grid;
grid-template-columns: 150px auto 150px;
grid-template-rows: 200px auto 200px;
This is just skimming the surface of CSS Grid. At least we are now armed with enough
knowledge to develop the sticky navbar and the sticky footer without Bootstrap.
A sticky navbar and footer without Bootstrap
It is nice to work with Bootstrap. CSS becomes less intimidating and approachable. They
also keep improving Bootstrap every year. However, there are also developments in
standard CSS which we tend to ignore because of the familiarity and convenience of
Bootstrap.
Let’s create a new project with a sticky navbar and footer without using Bootstrap.
c:\vibeprojects>cd noboots
c:\vibeprojects\noboots>
Open the new directory in VS Code and edit source\app.d to make it look like this:
import vibe.vibe;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.get("/", staticTemplate!"home.dt");
router.get("/about", staticTemplate!"about.dt");
router.get("/contact", staticTemplate!"contact.dt");
router.get("/login", staticTemplate!"login.dt");
html
head
title Employee Records System
include cssjs.dt
body
div.container
div.menu
include menu.dt
div.content
block changingcontent
div.footer
div.copyright Copyright © The Lorem Ipsum Company 2021
:css
body
{
margin: 0;
padding: 0;
}
.container
{
display: grid;
grid-template-rows: 40px auto 30px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
/*menu styles**************************************************/
.menu
{
position: fixed;
top: 0;
width: 100%;
padding: 10px 0;
background-color: whitesmoke;
}
.menu-item
{
display: inline;
}
.item-link
{
margin-left: 30px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 18px;
color: teal;
text-decoration: none;
}
.item-link-right
{
float: right;
margin-right: 30px;
}
/*end of menu styles*/
/*content styles**************************************************/
.content
{
padding: 40px 30px;
}
/*end of content styles/*
/*footer styles***************************************************/
.footer
{
position: fixed;
bottom: 0;
width: 100%;
background-color: whitesmoke;
padding: 5px 0;
}
.copyright
{
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 14px;
text-align: center;
color: teal;
}
.form-hidden
{
padding: 0;
margin: 0;
display: inline;
}
Then create views\menu.dt.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="about") About us
div.menu-item
a.item-link(href="contact") Contact us
div.menu-item
a.item-link.item-link-right(href="login") Login
extends layout
block changingcontent
h2 The Lorem Ipsum Company
div.
For the source text, <a href="https://lipsum.com/">go here.</a>
<br /><br />
What is Lorem Ipsum?<br /><br />
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley
of type and scrambled it to make a type specimen book. It has
survived not only five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged. It was
popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop
publishing software like Aldus PageMaker including versions of
Lorem Ipsum.
<br /><br />
extends layout
block changingcontent
h2 The About Us page
div.
<h3>This is the about us page.</h3>
views\contact.dt,
extends layout
block changingcontent
h2 The Contact Us page
div.
<h3>This is the contact us page.</h3>
and views\login.dt.
extends layout
block changingcontent
h2 The Login page
div.
<h3>This is the login page.</h3>
After you compile and run, you should be able to see the following pages:
In the views\cssjs.dt file, we declared this:
.container
{
display: grid;
grid-template-rows: 40px auto 30px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
Which means we only declared three rows and no columns using CSS Grid, effectively
making the whole page one column with three rows. The menu bar is on the top row and
the footer is on the bottom row, with the middle row expanding and contracting to fit the
page height.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="about") About us
div.menu-item
a.item-link(href="contact") Contact us
div.menu-item
a.item-link.item-link-right(href="#login") Login
div#login.modal-form
div.modal-form-wrapper-login
form.modal-form-grid(method="post", action="login")
input.text-
control(name="email", type="email", placeholder=" email address")
input.text-
control(name="password", type="password", placeholder=" password")
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
.modal-form
{
position: fixed;
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.5);
z-index: 99999;
opacity: 0;
pointer-events: none;
}
#login:target, #one_employee:target
{
opacity: 1;
pointer-events: auto;
}
.modal-form-grid {
display: grid;
grid-template: 30px 30px 30px / 1fr 1fr;
grid-gap: 10px;
}
.text-control
{
grid-column: 1 / 3;
}
#but-reset
{
grid-column: 1 / 2;
}
#but-submit
{
grid-column: 2 / 3;
}
.modal-form-wrapper-login
{
width: 250px;
position: absolute;
right: 0;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
.modal-form-wrapper-one_employee
{
width: 250px;
position: relative;
left: 280px;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
After you compile and run, click on the Login link and see the modal form in action:
Using CSS Grid, you notice that all the layout terms were declared in the CSS file so the
HTML is cleaner (compared to Bootstrap, that is).
Let us edit the menu to make it more relevant to our final project.
Edit menu.dt again to add the modal form for the #find_employee link
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="all_employees") All employees
div.menu-item
a.item-link(href="#find_employee") One employee
div.menu-item
a.item-link(href="new_employee") New employee
div.menu-item
a.item-link.item-link-right(href="#login") Login
div#login.modal-form
div.modal-form-wrapper-login
form.modal-form-grid(method="post", action="login")
input.text-
control(name="email", type="email", placeholder=" email address")
input.text-
control(name="password", type="password", placeholder=" password")
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
div#one_employee.modal-form
div.modal-form-wrapper-one_employee
form.modal-form-grid(method="post", action="find_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
Compile and run, then refresh the browser. Click on the One employee link.
That’s it for elementary design. I’m no expert on web design or CSS and I’m sure you
can find someone good with those.
The web framework
The web framework makes routing simpler and more intuitive as it combines business
logic and displaying web pages easily. Instead of declaring all the routes in the router
itself, the web framework makes use of conventions based on REST (Representational
State Transfer) to match URLs to actions or routes combined with the business logic.
For our purposes, we will focus on the GET and POST operations. We will use GET for
simple retrieval operations and use POST for the rest. For example, if the request is only
asking for a page to be displayed, we usually use the GET method. If the request is
asking for data to be added, retrieved, modified or deleted, we will use the POST method.
When a form is submitted, a GET method shows the input variables of the form at the
end of the URL, called the query string. With a POST method, they are not displayed.
Also, a query string has a limit in length while a POST method does not have this
limitation.
We don’t want to change or erase what we have done so far so we can preserve this point
in time and can go back to this state at any time, so we are going to create a new project
and simply copy all the \views\* files in this project to that new project.
Press Ctrl-C to stop the running program (twice if you have to).
c:\vibeprojects\noboots>cd ..
c:\vibeprojects>
c:\vibeprojects>cd empapp
c:\vibeprojects\empapp>
Simply copy the files in noboots\views\ into the current project empapp\views so we can
reuse them.
Open the new empapp folder in VS Code and edit source\app.d to make use of the web
framework.
import vibe.vibe;
import empinterface;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeInterface);
listenHTTP(settings, router);
runApplication();
}
The line
router.registerWebInterface(new EmployeeInterface);
brings in the web framework. The registerWebInterface() method of the router is the
facility that brings in all the goodies of the web framework. Here, the router is declaring
that the class EmployeeInterface, which we have not created yet, will have all the
facilities of the web framework.
We haven’t created the EmployeeInterface class yet, so let’s rectify that. We indicated
that we are importing the empinterface module, so let’s create that file.
module empinterface;
import vibe.vibe;
class EmployeeInterface
{
void index()
{
render!"home.dt";
}
void getFindEmployee()
{
response.writeBody("This is getFindEmployee");
}
}
So instead of creating separate methods, we can group them all together inside one class.
We don’t have to state the HTTPServerRequest and the HTTPServerResponse objects as
arguments for each and every method we define here because they are already available
to us if we need them as the variables request and response.
http://localhost:8080/
div#one_employee.modal-form
div.modal-form-wrapper-one_employee
form.modal-form-grid(method="get", action="find_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
http://localhost:8080/login
div#login.modal-form
div.modal-form-wrapper-login
form.modal-form-grid(method="post", action="login")
input.text-
control(name="email", type="email", placeholder=" email address", required
)
input.text-
control(name="password", type="password", placeholder=" password", require
d)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
The methods we defined in the EmployeeInterface class (you can give the class any
name), are merely stubs for now just to demonstrate the use of the web framework.
Using a web interface makes it easier to combine business logic with routing to make a
full application. You can mix business logic with displaying a page, such as accessing a
database before displaying the list of employees. That is what we will do next, but first
we have to set up our data source.
The data source
Presently there are two popular options for a database backend. We can use MySQL,
which is the most popular open-source DBMS, or we could use MongoDB, the most
popular NoSQL open-source document store.
With MySQL, we have to use a separate driver that is being maintained outside of
Vibe.d. With MongoDb, however, we do not need any external library as it is built-in
inside Vibe.d. So for now we will use MongoDB, then simply port the completed app to
MySQL.
Installing MongoDB
To download the MongoDB community server, head to
https://www.mongodb.com/try/download/community
Inside the Services window, look for the MongoDB Server (MongoDB), right-click on it
and click ‘Start’ to start the MongoDB server.
To test the MongoDB installation, on the terminal window, navigate to C:\Program
Files\MongoDB\Server\4.4\bin.
C:\Program Files\MongoDB\Server\4.4\bin>mongo.exe
MongoDB shell version v4.4.3
connecting to:
mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("9b423cb4-8414-4f73-a169-71db6fa33c76") }
MongoDB server version: 4.4.3
---
The server generated these startup warnings when booting:
2021-02-02T14:57:50.217-05:00: Access control is not enabled for the database.
Read and write access to data and configuration is unrestricted
---
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and
display metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL
accessible to you and anyone you share the URL with. MongoDB may use this information
to make product improvements and to suggest MongoDB products and deployment
options to you.
> quit()
C:\Program Files\MongoDB\Server\4.4\bin>
MongoDB Compass
Aside from the MongoDB console, we can also use the MongoDB Compass app to create
and manage databases, collections and records.
c:\Program Files\MongoDB\Server\4.4\bin>
When you look at MongoDB Compass again, you will see that the index was created.
The Lorem app is a simple employee records maintenance system of the imaginary
Lorem Ipsum Company, where user admins can view, add, edit and delete employee
records. These should cover the basic operations done on database tables and could easily
be adapted and extended for use on products or services instead of personnel.
The database is named empdb. The two collections under empdb are the admins and
employees collections.
For our purposes, we will use the ‘predefined schema’ below so we do not go awry with
our new-found freedom.
MongoDB automatically provides an indexed _id field. When a new document is added
and no _id is specified, a new _id is generated using a combination of timestamp and
random numbers to make a unique _id.
Accessing a MongoDB database
To access a MongoDB database from Vibe.d, we create a MongoClient connection, then
access the MongoDatabase object from that connection, then the particular
MongoCollection can be accessed through that MongoDatabase object. The
vibe.db.mongo.mongo module contains these classes.
Edit source\app.d:
import vibe.vibe;
import empinterface;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeInterface);
listenHTTP(settings, router);
runApplication();
}
Edit source\empinterface.d:
module empinterface;
import vibe.vibe;
import empmodelmongo;
class EmployeeInterface
{
void index()
{
render!"home.dt";
}
void getFindEmployee()
{
response.writeBody("This is getFindEmployee");
}
}
Here, we are importing the empmodelmongo module into our EmployeeInterface class.
We will try to separate the business logic code from the code that talks to the database.
The EmployeeModel class will contain the code that will talk to the database, leaving the
EmployeeInterface class with the business logic.
module empmodelmongo;
import vibe.db.mongo.mongo;
class EmployeeModel
{
private MongoCollection emptable, admins;
this()
{
MongoClient conn = connectMongoDB("localhost");
MongoDatabase empdb = conn.getDatabase("empdb");
emptable = empdb["employees"];
admins = empdb["admins"];
}
}
In D, this() is the class constructor and ~this() is the destructor. Whatever you want to
initialize at the creation of the object, you put inside the constructor.
Compile and run to see if there are any errors in accessing the MongoDB server.
It is easy to have errors as there are now two running servers communicating with each
other: our web server and the MongoDB server.
Dealing with (some) MongoDB-related errors
There are a so ways our code can go wrong, of course. Now that we have two running
servers (MongoDB and our web server), things get complicated further. Here are some of
what I encountered. Sometimes the errors may be caused by the MongoDB server not
running, but that can easily be resolved by starting the MongoDB server through the
Windows Services app.
C:\vibeprojects\empapp>
One way of resolving this is to change “localhost” to “127.0.0.1”, including the quotation
marks, making it a string. At least it is telling us which part of our code elicited an error
(line 11, column 36).
Sometimes the error messages do not indicate where in our code the error is.
C:\vibeprojects\empapp>
Although it is indicated which part of the code went wrong, we have no idea why. You
can try replacing localhost with 127.0.0.1, but sometimes it still would’nt work. Until in
your desperation you try creating a new project altogether and copying the same code,
then suddenly it works. I assure you that this code runs smoothly in my Ubuntu system.
Here is another error:
Running .\empapp.exe
vibe.db.mongo.connection.MongoDriverException@C:\Users\rey\AppData\Local\dub\packages\v
ibe-d-0.9.2\vibe-d\mongodb\vibe\db\mongo\connection.d(190): Failed to connect to
MongoDB server at localhost:27017.
----------------
0x00007FF667F56E46 in vibe.db.mongo.connection.MongoConnection.connect at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\connection.d(190)
0x00007FF667F525BC in vibe.db.mongo.client.MongoClient.this.__lambda2 at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(64)
0x00007FF667F553C6 in
vibe.core.connectionpool.ConnectionPool!(vibe.db.mongo.connection.MongoConnection).Conn
ectionPool.lockConnection at C:\Users\rey\AppData\Local\dub\packages\vibe-core-
1.13.0\vibe-core\source\vibe\core\connectionpool.d(94)
0x00007FF667F52B61 in vibe.db.mongo.client.MongoClient.lockConnection at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(170)
0x00007FF667F524BE in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(71)
0x00007FF667F522EB in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(41)
0x00007FF667F521E4 in vibe.db.mongo.mongo.connectMongoDB at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\mongo.d(91)
0x00007FF667F4ACE1 in empmodelmongo.EmployeeModel.this at
C:\vibeprojects\empapp\source\empmodelmongo.d(28)
0x00007FF667F4A140 in empinterface.EmployeeInterface.this at
C:\vibeprojects\empapp\source\empinterface.d(12)
0x00007FF667F22FB6 in D main at C:\vibeprojects\empapp\source\app.d(8)
0x00007FF6683FDD53 in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll().__lambda1()
0x00007FF6683FDB6F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF6683FDC7B in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll()
0x00007FF6683FDB6F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF6683FD976 in d_run_main2
0x00007FF6683C73C3 in d_run_main
0x00007FF667F25032 in app._d_cmain!().main at
C:\D\dmd2\windows\bin\..\..\src\druntime\import\core\internal\entrypoint.d(29)
0x00007FF6684DE858 in __scrt_common_main_seh at
d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
0x00007FFEE4167034 in BaseThreadInitThunk
0x00007FFEE4D3D0D1 in RtlUserThreadStart
object.Exception@C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(232): Failed to connect to [0:0:0:0:0:0:0:1]:27017: refused
----------------
0x00007FF667F24423 in std.exception.bailOut!(object.Exception).bailOut at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(516)
0x00007FF667F24329 in std.exception.enforce!().enforce!bool.enforce at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(437)
0x00007FF66829370D in vibe.core.net.connectTCP.__lambda5 at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(434)
0x00007FF668293389 in vibe.core.net.connectTCP at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(205)
0x00007FF668280941 in vibe.core.net.connectTCP at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(188)
0x00007FF667F567CA in vibe.db.mongo.connection.MongoConnection.connect at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(171)
0x00007FF667F525BC in vibe.db.mongo.client.MongoClient.this.__lambda2 at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(64)
0x00007FF667F553C6 in
vibe.core.connectionpool.ConnectionPool!(vibe.db.mongo.connection.MongoConnection).Conn
ectionPool.lockConnection at C:\Users\rey\AppData\Local\dub\packages\vibe-core-
1.13.0\vibe-core\source\vibe\core\connectionpool.d(94)
0x00007FF667F52B61 in vibe.db.mongo.client.MongoClient.lockConnection at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(170)
0x00007FF667F524BE in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(71)
0x00007FF667F522EB in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\client.d(41)
0x00007FF667F521E4 in vibe.db.mongo.mongo.connectMongoDB at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.2\vibe-
d\mongodb\vibe\db\mongo\mongo.d(91)
0x00007FF667F4ACE1 in empmodelmongo.EmployeeModel.this at
C:\vibeprojects\empapp\source\empmodelmongo.d(28)
0x00007FF667F4A140 in empinterface.EmployeeInterface.this at
C:\vibeprojects\empapp\source\empinterface.d(12)
0x00007FF667F22FB6 in D main at C:\vibeprojects\empapp\source\app.d(8)
0x00007FF6683FDD53 in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll().__lambda1()
0x00007FF6683FDB6F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF6683FDC7B in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll()
0x00007FF6683FDB6F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF6683FD976 in d_run_main2
0x00007FF6683C73C3 in d_run_main
0x00007FF667F25032 in app._d_cmain!().main at
C:\D\dmd2\windows\bin\..\..\src\druntime\import\core\internal\entrypoint.d(29)
0x00007FF6684DE858 in __scrt_common_main_seh at
d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
0x00007FFEE4167034 in BaseThreadInitThunk
0x00007FFEE4D3D0D1 in RtlUserThreadStart
Program exited with code 1
In this case, try changing localhost to 127.0.0.1. If that worked, then that might mean
your hosts file does not contain an entry that maps 127.0.0.1 to localhost. You can try
editing your hosts file, located in C:\Windows\System32\drivers\etc\.
Uncomment the line by deleting the hash mark (#). You will need to restart your
computer for the changes to take effect.
vibe.db.mongo.connection.MongoDriverException@C:\Users\rey\AppData\Local\dub\packages\v
ibe-d-0.9.3\vibe-d\mongodb\vibe\db\mongo\connection.d(190): Failed to connect to
MongoDB server at 127.0.0.1:27017.
----------------
0x00007FF7DBA2B146 in vibe.db.mongo.connection.MongoConnection.connect at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\connection.d(190)
0x00007FF7DBA2999C in vibe.db.mongo.client.MongoClient.this.__lambda2 at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(64)
0x00007FF7DBA359E6 in
vibe.core.connectionpool.ConnectionPool!(vibe.db.mongo.connection.MongoConnection).Conn
ectionPool.lockConnection at C:\Users\rey\AppData\Local\dub\packages\vibe-core-
1.13.0\vibe-core\source\vibe\core\connectionpool.d(94)
0x00007FF7DBA29F41 in vibe.db.mongo.client.MongoClient.lockConnection at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(170)
0x00007FF7DBA2989E in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(71)
0x00007FF7DBA296CB in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(41)
0x00007FF7DBA32F24 in vibe.db.mongo.mongo.connectMongoDB at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\mongo.d(91)
0x00007FF7DBA26A81 in empmongo.EmployeeModel.this at
C:\vibeprojects\lorem\source\empmongo.d(34)
0x00007FF7DBA24F20 in empcontrol.EmployeeController.this at
C:\vibeprojects\lorem\source\empcontrol.d(45)
0x00007FF7DB9D3D56 in D main at C:\vibeprojects\lorem\source\app.d(8)
0x00007FF7DBED8D33 in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll().__lambda1()
0x00007FF7DBED8B4F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF7DBED8C5B in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll()
0x00007FF7DBED8B4F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF7DBED8956 in d_run_main2
0x00007FF7DBEA2783 in d_run_main
0x00007FF7DB9D65C2 in app._d_cmain!().main at
C:\D\dmd2\windows\bin\..\..\src\druntime\import\core\internal\entrypoint.d(29)
0x00007FF7DBFBA558 in __scrt_common_main_seh at
d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
0x00007FFFBFB67034 in BaseThreadInitThunk
0x00007FFFC01BD241 in RtlUserThreadStart
object.Exception@C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(232): Failed to connect to 127.0.0.1:27017: refused
----------------
0x00007FF7DB9D5403 in std.exception.bailOut!(object.Exception).bailOut at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(516)
0x00007FF7DB9D5309 in std.exception.enforce!().enforce!bool.enforce at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(437)
0x00007FF7DBD66A7D in vibe.core.net.connectTCP.__lambda5 at
C:\D\dmd2\windows\bin\..\..\src\phobos\std\exception.d(434)
0x00007FF7DBD666F9 in vibe.core.net.connectTCP at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(205)
0x00007FF7DBD57211 in vibe.core.net.connectTCP at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(188)
0x00007FF7DBA2AACA in vibe.db.mongo.connection.MongoConnection.connect at
C:\Users\rey\AppData\Local\dub\packages\vibe-core-1.13.0\vibe-
core\source\vibe\core\net.d(171)
0x00007FF7DBA2999C in vibe.db.mongo.client.MongoClient.this.__lambda2 at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(64)
0x00007FF7DBA359E6 in
vibe.core.connectionpool.ConnectionPool!(vibe.db.mongo.connection.MongoConnection).Conn
ectionPool.lockConnection at C:\Users\rey\AppData\Local\dub\packages\vibe-core-
1.13.0\vibe-core\source\vibe\core\connectionpool.d(94)
0x00007FF7DBA29F41 in vibe.db.mongo.client.MongoClient.lockConnection at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(170)
0x00007FF7DBA2989E in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(71)
0x00007FF7DBA296CB in vibe.db.mongo.client.MongoClient.this at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\client.d(41)
0x00007FF7DBA32F24 in vibe.db.mongo.mongo.connectMongoDB at
C:\Users\rey\AppData\Local\dub\packages\vibe-d-0.9.3\vibe-
d\mongodb\vibe\db\mongo\mongo.d(91)
0x00007FF7DBA26A81 in empmongo.EmployeeModel.this at
C:\vibeprojects\lorem\source\empmongo.d(34)
0x00007FF7DBA24F20 in empcontrol.EmployeeController.this at
C:\vibeprojects\lorem\source\empcontrol.d(45)
0x00007FF7DB9D3D56 in D main at C:\vibeprojects\lorem\source\app.d(8)
0x00007FF7DBED8D33 in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll().__lambda1()
0x00007FF7DBED8B4F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF7DBED8C5B in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).runAll()
0x00007FF7DBED8B4F in void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int
function(char[][])*).tryExec(scope void delegate())
0x00007FF7DBED8956 in d_run_main2
0x00007FF7DBEA2783 in d_run_main
0x00007FF7DB9D65C2 in app._d_cmain!().main at
C:\D\dmd2\windows\bin\..\..\src\druntime\import\core\internal\entrypoint.d(29)
0x00007FF7DBFBA558 in __scrt_common_main_seh at
d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
0x00007FFFBFB67034 in BaseThreadInitThunk
0x00007FFFC01BD241 in RtlUserThreadStart
Program exited with code 1
C:\vibeprojects\lorem>
In this case, simply open the Services app of Windows and start MongoDB.
After starting the MongoDB server, try compiling and running again.
C:\vibeprojects\lorem>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
taggedalgebraic 0.11.19: target for configuration "library" is up to date.
eventcore 0.9.13: target for configuration "winapi" is up to date.
stdx-allocator 2.77.5: target for configuration "library" is up to date.
vibe-core 1.13.0: target for configuration "winapi" is up to date.
vibe-d:utils 0.9.3: target for configuration "library" is up to date.
vibe-d:data 0.9.3: target for configuration "library" is up to date.
mir-linux-kernel 1.0.1: target for configuration "library" is up to date.
vibe-d:crypto 0.9.3: target for configuration "library" is up to date.
diet-ng 1.7.4: target for configuration "library" is up to date.
vibe-d:stream 0.9.3: target for configuration "library" is up to date.
vibe-d:textfilter 0.9.3: target for configuration "library" is up to date.
vibe-d:inet 0.9.3: target for configuration "library" is up to date.
vibe-d:tls 0.9.3: target for configuration "openssl-mscoff" is up to date.
vibe-d:http 0.9.3: target for configuration "library" is up to date.
vibe-d:mail 0.9.3: target for configuration "library" is up to date.
vibe-d:mongodb 0.9.3: target for configuration "library" is up to date.
vibe-d:redis 0.9.3: target for configuration "library" is up to date.
vibe-d:web 0.9.3: target for configuration "library" is up to date.
vibe-d 0.9.3: target for configuration "vibe-core" is up to date.
lorem ~master: target for configuration "application" is up to date.
To force a rebuild of up-to-date targets, run again with --force.
Copying files for vibe-d:tls...
Running .\lorem.exe
[main(----) INF] Listening for requests on http://[::1]:8080/
[main(----) INF] Listening for requests on http://127.0.0.1:8080/
If you can refresh the browser without errors then you did good..
The EmployeeModel class
A web data-entry form in Vibe.d needs a structure from which we can extract the fields
for saving into the database table. It makes things simpler if the database and the form
have a one-to-one correspondence in field names.
We can create a struct data structure that mimics an employees record. We will call that
struct Employee.
Then we can create a class that contains the methods to manipulate that struct, such as
add, edit, get and delete, in short, all the CRUD operations (create, read, update, delete)
on the MongoDB server. It is a model that represents one record of the employees table
on the MongoDB server, so we will call that class EmployeeModel.
We are going to presume that this app will only be deployed in an intranet setting and
only a few people have access, and that there is a rare chance of more than one person
using the app at the same time.
Then copy the files inside empapp\views\ folder of the previous project to save us time.
Create source\empmongo.d.
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
}
The addEmployee() method show how to insert a record in MongoDb in Vibe.d syntax.
The native MongoDB syntax is very, very similar.
We need to add new records to the blank employees collection so we should create an
employee form. That data-entry form will use the Employee struct. Let’s do that next.
The form for adding a new employee record
Edit views\menu.dt to add the New employee link.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="#find") Find employee
div.menu-item
a.item-link(href="list_employees") Show employees
div.menu-item
a.item-link(href="new_employee") New employee
div#find.modal-form
div.modal-form-wrapper-find
form.modal-form-grid(method="post", action="show_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
Then create the views\empnew.dt page for our data entry form.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 New employee details
form.form-grid(method="post", action="new_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#deprt.form-grid-column-input(name="e_deprt")
- foreach(dep; deps)
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", placeholder="Salary grade")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", placeholder="email@address.com", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", placeholder="password", required)
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", placeholder="First name", required)
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", placeholder="Last name", required)
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", placeholder="Phone number")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", placeholder="Street address", required)
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", placeholder="City", required)
div.form-grid-column Province<br />
select#province.form-grid-column-input(name="e_province")
- foreach(prov; provs)
option(value="#{prov[0]}") #{prov[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", placeholder="A1A 1A1")
div.form-grid-column ID Picture<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo")
input(type="hidden", name="e__id", value="123456789012345678901234")
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
The line
will call the postNewEmployee() method (which we haven’t created yet) of the
EmployeeController class.
enctype="multipart/form-data"
means the form will also upload a file, which in this case is a photo.
Create source\empcontrol.d:
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee()
{
render!("empnew.dt", deps, provs);
}
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
}
We created the deps and the provs array variables for forms that require drop-down
options for departments and provinces.
The function
render!("index.dt");
is a variadic function template. It means render!() can have a varying number of
parameters (arguments) of different data types. We have seen that construct before with
staticTemplate!().
This time render!() has three arguments, one string and two arrays.
The file being uploaded is in request.files. Remember that the HTTPServerRequest and
the HTTPServerResponse are available to us as the request and response variables.
So the line
means we are extracting the uploaded file from request.files into the pic variable.
In the empnew.dt form, the picture variable corresponds to the name we gave to the
button field:
input.form-grid-column-input(type="file", name="picture")
The data-entry form requires a photo image file to be uploaded. Uploaded image files are
usually not saved into the database as that would easily make the database bloated, so we
need to save them into a folder which, if not already existing, we should create. Hence we
need to import some libraries related to file operations:
import std.file;
import std.path;
import std.algorithm;
The passwords need to be encrypted before they are saved so even if the database is
hacked and bad guys gained access, the passwords are still safely unreadable. The
employees may need to have access to their own data in the future, such as access to time
worked and salary info, so we need to keep their passwords safe.
import vibe.http.auth.digest_auth;
Vibe.d sometimes emit errors when importing data from MongoDB when there are
missing fields because they might have been blank when the record was inserted, so we
want to avoid blank fields. Vibe.d has some data validation methods but for now we will
use this crude method and be content that frontend is enough.
If some of the non-required fields are blank, we simply give them dummy data:
import vibe.vibe;
import empcontrol;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeController);
listenHTTP(settings, router);
runApplication();
}
Then try to compile. If you get errors (the views\empnew.dt is a big file), they may be
just simple typographical errors, like this:
It is a very long error message, but the culprit is just a typo in views\empnew.dt, which is
e__id (double underscore) instead of id.
Once you resolved the errors, compile, run and refresh your browser.
Then click on New employee and enter some sample data to test the new data-entry
system.
Not Found
void postNewEmployee(Employee e)
{
auto pic = "picture" in request.files;
if(pic !is null)
{
string photopath = "none yet";
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
e.photo = photopath;
}
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
The line
redirect("list_employees");
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
This method returns an Employee array, meaning, not just one record but all the records
in the emptable collection. MongoDB saves documents (records) as Bson (binary JSON)
objects, so we deserialize them using the Employee struct as the map.
If we use emptable.find() without any arguments, that means we want all the records.
extends layout
block maincontent
include csstable.dt
div.table-wrapper
table
tr
td.no-border(colspan="11")
tr
th Department
th First name
th Last name
th Phone number
th Email address
th Salary grade
th Street address
th City
th Province
th PostCode
th Action
- foreach(e; emps)
tr
td #{e.deprt}
td #{e.fname}
td #{e.lname}
td #{e.phone}
td #{e.email}
td #{e.paygd}
td #{e.street}
td #{e.city}
td #{e.province}
td #{e.postcode}
td
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/pen.ico")
|
form.form-hidden(method="get", action="delete_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/trash.ico")
|
- foreach(e; emps)
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
- foreach(e; emps)
which will iterate through all of emps, which is an array of Employees, with e
representing the current Employee record.
To display the value of a variable passed to the template, we use the construct #{variable}.
This is equivalent to JSP’s <%= variable %>
td #{e.deprt}
:css
.table-wrapper
{
margin: 20px auto;
padding: 20px;
}
table
{
margin: 0 auto;
padding: 20px;
border-collapse: collapse;
}
table td, table th
{
border: 1px solid black;
margin: 0;
padding: 0 5px;
}
.no-border
{
border: 0;
}
Compile and run the app and refresh the browser. Click on New employee again and add
an employee.
Then click the Submit button.
I got the icons for the pencil and the trash bin by simply googling for ‘pencil icon’ and
‘trash bin icon’ and saving the icons into the \public\images\ folder.
input(type="image", src="images/trash.ico")
so save and name the icon files in the \public\images\ folder with the same names.
Add more sample records by clicking on the New employee link after each submission.
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
}
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee()
{
render!("empnew.dt", deps, provs);
}
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
}
There is a hidden field in the emplist.dt file: the id field, which has the value of the _id
field in the database table, and which we can use as a key to retrieve the record so we can
populate the form with the employee data.
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/pen.ico")
BsonObjectID is the Vibe.d data type of the _id field of the database. This method returns
an Employee struct after retrieving the record from the database using the _id as the key.
This method calls the empModel.getEmployee() method with the id as the argument, then
receives the returned Employee with the _id field equal to the id variable data and uses it
to render the form empedit.dt, which we should create next.
Create views\empedit.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Edit employee details
form.form-grid(method="post", action="edit_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#depid.form-grid-column-input(name="e_deprt", value="#{e.deprt}")
- foreach(dep; deps)
- if(dep == e.deprt)
option(value="#{dep}", selected) #{dep}
- else
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", value="#{e.paygd}")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", value="#{e.email}")
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", value="#{e.pword}")
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", value="#{e.fname}")
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", value="#{e.lname}")
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", value="#{e.phone}")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", value="#{e.street}")
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", value="#{e.city}")
div.form-grid-column Province<br />
select#province.form-grid-column-
input(name="e_province", value="#{e.province}")
- foreach(p; provs)
- if(p[0] == e.province)
option(value="#{p[0]}", selected) #{p[1]}
- else
option(value="#{p[0]}") #{p[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", value="#{e.postcode}")
div.form-grid-column ID Picture:<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo", value="#{e.photo}")
input(type="hidden", name="e__id", value="#{e._id}")
div.form-grid-column
br
a(href="list_employees")
button.form-grid-column-button(type="button") Cancel
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
The form is displayed using the Employee data that was passed to it.
Compile, run and refresh the browser and click on a pencil icon to edit a record.
Great, a record was opened for editing. After clicking submit, we get this error:
It means we haven’t defined the functions to save the changes to the database. So let’s do
it.
Saving the form changes to the database
Add this method at the end of source\empcontrol.d.
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
The only differences between this method and the postNewEmployee() method are the
first line and the second to the last line, which is the call to empModel.editEmployee()
method, which we haven’t defined yet.
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
This code means look for the record with the value of the variable id in the _id field, then
edit that record with this data.
Compile, run and refresh the browser and edit some fileds.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmysql;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee()
{
render!("empnew.dt", deps, provs);
}
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
}
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
}
Time to implement the empModel.delete() function to activate the trash bin icon.
Deleting records in the database
When a user clicks on the trash bin icon, it should not immediately delete the
corresponding record. The user should be given the chance to recover if he/she made a
mistake. We can simply display a page showing the employee details first, then ask the
user for confirmation.
Create views\empdelete.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2.brown Are you sure you want to delete this record?
form.form-grid(method="post", action="delete_employee")
div.form-grid-column
| Department:<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade:<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address:<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password:<br />
div.form-grid-column-field ********
div.form-grid-column
| First name:<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name:<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone:<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city):<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City:<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province:<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code:<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture:<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="email", value="#{e.email}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") No
div.form-grid-column
input.form-grid-column-button(type="submit", value="Yes")
Here we displayed a page showing the employee details and asking the user for
confirmation.
Compile, run and refresh the browser. Show the list of employees again and click on a
trash bin icon. The confirmation page should show.
Click No for now just to test if the No button works. Then click the trash bin icon for
another record.
This time click the Yes button.
Compile, run and refresh the browser to the list of employees. Click on a trash bin icon,
click Yes on the next screen, and you will be redirected to the list of employees again.
This time you should see that the record you selected is no longer listed.
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee()
{
render!("empnew.dt", deps, provs);
}
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
void getDeleteEmployee(BsonObjectID id)
{
Employee e = empModel.getEmployee(id);
render!("empdelete.dt", e);
}
This time, we will find an employee by the first name and last name.
Instead of looking for the ID, this time the method is looking for the first name and last
name.
Create views\empshow.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Employee details
form.form-grid(method="get", action="edit_employee")
div.form-grid-column
| Department<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password<br />
div.form-grid-column-field ********
div.form-grid-column
| First name<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city)<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="id", value="#{e._id}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") Close
div.form-grid-column
input.form-grid-column-button(type="submit", value="Edit")
Then compile, run and refresh the browser. Look for an employee and click Submit.
Click on the Edit button. It will show the same empedit.dt file we created previously.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee()
{
render!("empnew.dt", deps, provs);
}
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
void getListEmployees()
{
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps);
}
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
When an update attempt failed, the error should also be shown to let the user know the
state of things.
After a failed search when the employee was not found, the message should be also be
displayed.
When a login attempt failed, the login page should display again with an error message
telling the user why the login attempt failed.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
void index()
{
render!("index.dt");
}
@errorDisplay!getNewEmployee
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@errorDisplay!getListEmployees
void getShowEmployee(string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
that getEditEmployee() will be called when there is an error during processing, the
getEditEmployee() method now has an argument named _error.
The _error argument is assigned null as a default in case no errors were generated. We
can now pass it to the empedit.dt template.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Edit employee details
form.form-grid(method="post", action="edit_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#depid.form-grid-column-input(name="e_deprt", value="#{e.deprt}")
- foreach(dep; deps)
- if(dep == e.deprt)
option(value="#{dep}", selected) #{dep}
- else
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", value="#{e.paygd}")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", value="#{e.email}")
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", value="#{e.pword}")
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", value="#{e.fname}")
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", value="#{e.lname}")
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", value="#{e.phone}")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", value="#{e.street}")
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", value="#{e.city}")
div.form-grid-column Province<br />
select#province.form-grid-column-
input(name="e_province", value="#{e.province}")
- foreach(p; provs)
- if(p[0] == e.province)
option(value="#{p[0]}", selected) #{p[1]}
- else
option(value="#{p[0]}") #{p[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", value="#{e.postcode}")
div.form-grid-column ID Picture:<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo", value="#{e.photo}")
input(type="hidden", name="e__id", value="#{e._id}")
div.form-grid-column
br
a(href="list_employees")
button.form-grid-column-button(type="button") Cancel
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}
extends layout
block maincontent
include csstable.dt
div.table-wrapper
table
tr
td.no-border(colspan="11")
- if(error)
span.error #{error}<br /><br />
tr
th Department
th First name
th Last name
th Phone number
th Email address
th Salary grade
th Street address
th City
th Province
th PostCode
th Action
- foreach(e; emps)
tr
td #{e.deprt}
td #{e.fname}
td #{e.lname}
td #{e.phone}
td #{e.email}
td #{e.paygd}
td #{e.street}
td #{e.city}
td #{e.province}
td #{e.postcode}
td
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/pen.ico")
|
form.form-hidden(method="get", action="delete_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/trash.ico")
|
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 New employee details
form.form-grid(method="post", action="new_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#deprt.form-grid-column-input(name="e_deprt")
- foreach(dep; deps)
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", placeholder="Salary grade")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", placeholder="email@address.com", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", placeholder="password", required)
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", placeholder="First name", required)
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", placeholder="Last name", required)
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", placeholder="Phone number")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", placeholder="Street address", required)
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", placeholder="City", required)
div.form-grid-column Province<br />
select#province.form-grid-column-input(name="e_province")
- foreach(prov; provs)
option(value="#{prov[0]}") #{prov[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", placeholder="A1A 1A1")
div.form-grid-column ID Picture<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo")
input(type="hidden", name="e__id", value="123456789012345678901234")
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}
Compile, run and refresh the browser. Click on Find employee and enter a non-existent
record and click Submit.
So now we can display error messages.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmysql;
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
@errorDisplay!getNewEmployee
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@errorDisplay!getListEmployees
void getShowEmployee(string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
}
Time to secure our site so that only authorized users can make changes to the data.
Authentication means verifying that the user is who he/she says he/she is. Authorization
means determining which parts of the app the user is allowed access.
We removed the Login menu item before. Time to put it back on the menu (pun intended),
but this time it should point to a regular form instead of a modal form.
Edit views\menu.dt.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="#find") Find employee
div.menu-item
a.item-link(href="list_employees") Show employees
div.menu-item
a.item-link(href="new_employee") New employee
div.menu-item
a.item-link.item-link-right(href="login") Login
div#find.modal-form
div.modal-form-wrapper-find
form.modal-form-grid(method="get", action="show_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
void getLogin()
{
render!"login.dt";
}
The code is simply rendering the login.dt template file.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Please log in first
form.form-grid(method="post", action="login")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="email", placeholder="email", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="password", placeholder="password", required)
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
Click on the Login link on the menu to show the login page.
But when you try to login, you get this error:
Let us fix that. But first, let us encrypt the passwords in the admins collection.
Looking at the data in the admins collection, we see that the passwords remain raw and
readable instead of encrypted so it needs to be rectified.
Encrypting the passwords in the admins table
Edit views\menu.dt.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="#find") Find employee
div.menu-item
a.item-link(href="list_employees") Show employees
div.menu-item
a.item-link(href="encrypt_passwords") Encrypt passwords
div.menu-item
a.item-link(href="new_employee") New employee
div.menu-item
a.item-link.item-link-right(href="login") Login
div#find.modal-form
div.modal-form-wrapper-find
form.modal-form-grid(method="get", action="show_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
void getEncryptPasswords()
{
Admin[] admins = empModel.getAdmins;
foreach(a; admins)
{
auto pword = createDigestPassword(realm, a.email, a.password);
empModel.replacePassword(a.email, pword);
}
redirect("/");
}
Admin[] getAdmins()
{
Admin[] admins;
auto result = admintable.find();
foreach(doc; result) admins ~= deserializeBson!Admin(doc);
return admins;
}
There must be a more efficient way to update the same field in all records in one go, but
since I am no MongoDB expert, I’ll leave that to you.
Compile, run and refresh the bowser, then click on the Encrypt passwords link.
It looks as if nothing happened. Verify with Compass if the passwords were really
encrypted by clicking on the REFRESH button.
The passwords were really encrypted. Now we can remove the temporary link from the
menu, but we can let the code for encrypting passwords of the admins table stay for
future use.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="#find") Find employee
div.menu-item
a.item-link(href="list_employees") Show employees
div.menu-item
a.item-link(href="new_employee") New employee
div.menu-item
a.item-link.item-link-right(href="login") Login
div#find.modal-form
div.modal-form-wrapper-find
form.modal-form-grid(method="get", action="show_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
class EmployeeController
{
private EmployeeModel empModel;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
@errorDisplay!getNewEmployee
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@errorDisplay!getListEmployees
void getShowEmployee(string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
render!("empshow.dt", e);
}
void getLogin()
{
render!"login.dt";
}
void getEncryptPasswords()
{
Admin[] admins = empModel.getAdmins;
foreach(a; admins)
{
auto pword = createDigestPassword(realm, a.email, a.password);
empModel.replacePassword(a.email, pword);
}
redirect("/");
}
}
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
Admin[] getAdmins()
{
Admin[] admins;
auto result = admintable.find();
foreach(doc; result) admins ~= deserializeBson!Admin(doc);
return admins;
}
Compile, run and refresh the browser, then try to log in.
Click the Submit button. If the list of employees is shown, you did good.
Now let’s save the login state to the session.
Saving the login state to the session
A session is when a user logs in up to the point when the user logs out. When we want to
retrieve a particular record from the database, we simply use the _id field as the key to
retrieve the record by passing the key to the next page.
When a user logs in, the log in state needs to be saved for the duration of the session so
that the user can navigate from page to page without having to log in again and again.
If the user is logged in, we should save that state so that as the user navigates from page
to page, the system can check if the user is authorized to access that particular page.
A variable, such as a logged-in state, can be saved to this session facility. Vibe.d has such
a facility for saving session variables. It is called the session store, of which there are two
kinds: MemorySessionStore and the database-based store. For now, we will use the
MemorySessionStore kind.
Edit source\app.d.
import vibe.vibe;
import empcontrol;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeController);
listenHTTP(settings, router);
runApplication();
}
Edit source\empcontrol.d.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
void getNewEmployee(string _error = null)
{
string error = _error;
render!("empnew.dt", deps, provs, error);
}
@errorDisplay!getNewEmployee
void postNewEmployee(Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@errorDisplay!getEditEmployee
void postEditEmployee(Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@errorDisplay!getListEmployees
void getShowEmployee(string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
void getEncryptPasswords()
{
Admin[] admins = empModel.getAdmins;
foreach(a; admins)
{
auto pword = createDigestPassword(realm, a.email, a.password);
empModel.replacePassword(a.email, pword);
}
redirect("/");
}
@errorDisplay!getLogin
void postLogin(string email, string password)
{
auto pword = createDigestPassword(realm, email, password);
bool isAdmin = empModel.getAdmin(email, pword);
enforce(isAdmin, "Email and password combination not found!");
User user = m_user;
user.loggedIn = true;
user.email = email;
m_user = user;
redirect("list_employees");
}
}
We created a User struct to hold the logged-in state and will be a session variable.
struct User
{
bool loggedIn;
string email;
}
We changed the getLogin() method to accept an _error variable from the postLogin()
method if if encounters an error.
We indicated that getLogin() will be called and passed the error if postLogin() encounters
an error.
@errorDisplay!getLogin
And we also changed the value of m_user inside postLogin(), which starts the session.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Please log in first
form.form-grid(method="post", action="login")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="email", placeholder="email", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="password", placeholder="password", required)
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}<br /><br />
Compile, run and refresh the browser. Click on the Login menu item and input a non-
existing admin user.
Next is how to let the other pages know about the logged-in state.
Enforcing authorization with the session variable
We want to restrict access to all the pages in the website except the home page and the
login page, but right now, clicking on any of the links on the menu shows the page.
Meaning, all the links in the menu are non-restrictive and is viewable by anyone. We
need to rectify that.
Only the home page and the login page should be accessible to anyone and the rest
should be accessible only to logged-in users. There should be a way to check the session
variable if the user is logged in or not before deciding to open the page.
The @auth annotation is a shortcut for calling the ensureAuth() function to check if the
user is logged in before running a method.
That mixin statement there is needed to make this private function accessible.
This function redirects to the getLogin() method if the user is not logged in yet.
so we can just use @auth to mean we are calling the ensureAuth() private function, like
this:
@auth
void getNewEmployee(string _error = null)
The @auth annotation means call the ensureAuth() function, which checks the logged-in
state, before running this getNewEmployee() method.
Since the ensureAuth() function is passing the _authUser variable, we now have to add it
as an argument to all the methods that call ensureAuth(), like this:
@auth
void getNewEmployee(string _authUser, string _error = null)
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmysql;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
render!("index.dt");
}
@auth
void getNewEmployee(string _authUser, string _error = null)
{
string error = _error;
render!("empnew.dt", deps, provs, error);
}
@errorDisplay!getNewEmployee
@auth
void postNewEmployee(string _authUser, Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@auth
void getListEmployees(string _authUser, string _error = null)
{
string error = _error;
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps, error);
}
@auth
void getEditEmployee(string _authUser, int id, string _error = null)
{
string error = _error;
Employee e = empModel.getEmployee(id);
render!("empedit.dt", e, deps, provs, error);
}
@errorDisplay!getEditEmployee
@auth
void postEditEmployee(string _authUser, Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@auth
void getDeleteEmployee(string _authUser, int id)
{
Employee e = empModel.getEmployee(id);
render!("empdelete.dt", e);
}
@auth
void postDeleteEmployee(string _authUser, int id)
{
empModel.deleteEmployee(id);
redirect("list_employees");
}
@errorDisplay!getListEmployees
@auth
void getShowEmployee(string _authUser, string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
@errorDisplay!getLogin
void postLogin(string email, string password)
{
auto pword = createDigestPassword(realm, email, password);
bool isAdmin = empModel.getAdmin(email, pword);
enforce(isAdmin, "Email and password combination not found!");
User user = m_user;
user.loggedIn = true;
user.email = email;
m_user = user;
redirect("list_employees");
}
We added the @auth annotation for each method that requires authorization.
We did not make any changes to the source\empmongo.d, so we are good.
Compile, run and refresh the browser. Click any link on the menu except the Home link
and the Login link and you will be redirected to the login page.
Now we are assured that sensitive data is protected and accessible only to authorized
users.
Let’s just decide that if the user clicks on the Home link on the menu, the user is logged
out.
void index()
{
m_user = User.init;
terminateSession;
render!("index.dt");
}
The .init method is part of a user-defined data type such as a struct. It initializes all the
members of the struct and gives each its default initial value.
Here, we re-initialized the m_user session variable then explicitly terminated the session
before displaying the index.dt template page.
Compile, run, refresh the browser and test the app. You will see that when you click on
the Home link, you have to log in again to see the other pages.
Now, since the login page automatically shows up if the user is not logged in yet, the
Login link on the menu looks superfluous. We can get rid of it then.
Edit views\menu.dt and remove the Login link.
div.menu-item
a.item-link(href="/") Home
div.menu-item
a.item-link(href="#find") Find employee
div.menu-item
a.item-link(href="list_employees") Show employees
div.menu-item
a.item-link(href="new_employee") New employee
div#find.modal-form
div.modal-form-wrapper-find
form.modal-form-grid(method="get", action="show_employee")
input.text-
control(name="fname", type="text", placeholder=" First name", required)
input.text-
control(name="lname", type="text", placeholder=" Last name", required)
input#but-reset(type="reset", value="Clear")
input#but-submit(type="submit", value="Submit")
a.close(href="#close") Cancel
Compile, run, refresh the browser and test the app again. This time, no more Login link.
With that, we are done with our project using MongoDB. Hurray!
All the source code
So here is the full source\app.d.
import vibe.vibe;
import empcontrol;
void main()
{
auto router = new URLRouter;
router.get("*", serveStaticFiles("public/"));
router.registerWebInterface(new EmployeeController);
listenHTTP(settings, router);
runApplication();
}
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmongo;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
m_user = User.init;
terminateSession;
render!("index.dt");
}
@auth
void getNewEmployee(string _authUser, string _error = null)
{
string error = _error;
render!("empnew.dt", deps, provs, error);
}
@errorDisplay!getNewEmployee
@auth
void postNewEmployee(string _authUser, Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@auth
void getListEmployees(string _authUser, string _error = null)
{
string error = _error;
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps, error);
}
@auth
void getEditEmployee(string _authUser, BsonObjectID id, string _error = null)
{
string error = _error;
Employee e = empModel.getEmployee(id);
render!("empedit.dt", e, deps, provs, error);
}
@errorDisplay!getEditEmployee
@auth
void postEditEmployee(string _authUser, Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@auth
void getDeleteEmployee(string _authUser, BsonObjectID id)
{
Employee e = empModel.getEmployee(id);
render!("empdelete.dt", e);
}
@auth
void postDeleteEmployee(string _authUser, BsonObjectID id)
{
empModel.deleteEmployee(id);
redirect("list_employees");
}
@errorDisplay!getListEmployees
@auth
void getShowEmployee(string _authUser, string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
@errorDisplay!getLogin
void postLogin(string email, string password)
{
auto pword = createDigestPassword(realm, email, password);
bool isAdmin = empModel.getAdmin(email, pword);
enforce(isAdmin, "Email and password combination not found!");
User user = m_user;
user.loggedIn = true;
user.email = email;
m_user = user;
redirect("list_employees");
}
module empmongo;
import vibe.db.mongo.mongo;
struct Employee
{
BsonObjectID _id;
string deprt;
string email;
string pword;
string fname;
string lname;
string phone;
string paygd;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string password;
}
class EmployeeModel
{
MongoCollection emptable, admintable;
this()
{
auto empdb = connectMongoDB("127.0.0.1").getDatabase("empdb");
emptable = empdb["employees"];
admintable = empdb["admins"];
}
void addEmployee(Employee e)
{
emptable.insert
([
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]);
}
Employee[] getEmployees()
{
Employee[] emps;
auto results = emptable.find();
foreach(doc;results) emps ~= deserializeBson!Employee(doc);
return emps;
}
void editEmployee(Employee e)
{
emptable.update
(
["_id":e._id],
["$set":
[
"deprt": e.deprt,
"email": e.email,
"pword": e.pword,
"fname": e.fname,
"lname": e.lname,
"phone": e.phone,
"paygd": e.paygd,
"photo": e.photo,
"street": e.street,
"city": e.city,
"province": e.province,
"postcode": e.postcode
]
]
);
}
Admin[] getAdmins()
{
Admin[] admins;
auto result = admintable.find();
foreach(doc; result) admins ~= deserializeBson!Admin(doc);
return admins;
}
The cssformgrid.dt.
:css
.form-grid-wrapper
{
width: 700px;
height: auto;
margin: 20px auto;
padding: 5px 20px;
border-radius: 10px;
background-color: whitesmoke;
}
.form-grid
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.form-grid-column
{
display: inline;
height: auto;
padding: 5px;
}
.form-grid-column-field
{
width: 100%;
font-size: 16px;
border-bottom: 1px solid black;
padding: 5px 0;
}
.form-grid-column-button
{
width: 100%;
font-size: 16px;
height: 40px;
margin-top: 10px;
}
.form-grid-column-input
{
width: 100%;
font-size: 16px;
}
The cssjs.dt.
:css
body
{
margin: 0;
padding: 0;
}
.container
{
display: grid;
grid-template-rows: 40px auto 30px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.error
{
color: brown;
font-weight: bold;
}
.text-right
{
text-align: right;
}
/*menu styles**************************************************/
.menu
{
position: fixed;
top: 0;
width: 100%;
padding: 10px 0;
background-color: whitesmoke;
}
.menu-item
{
display: inline;
}
.item-link
{
margin-left: 30px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 18px;
color: teal;
text-decoration: none;
}
.item-link-right
{
float: right;
margin-right: 30px;
}
.modal-form
{
position: fixed;
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.5);
z-index: 99999;
opacity: 0;
pointer-events: none;
}
#find:target
{
opacity: 1;
pointer-events: auto;
}
.modal-form-grid {
display: grid;
grid-template: 30px 30px 30px / 1fr 1fr;
grid-gap: 10px;
}
.text-control
{
grid-column: 1 / 3;
}
#but-reset
{
grid-column: 1 / 2;
}
#but-submit
{
grid-column: 2 / 3;
}
.modal-form-wrapper-login
{
width: 250px;
position: absolute;
right: 0;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
.modal-form-wrapper-find
{
width: 250px;
position: relative;
left: 120px;
margin-top: 40px;
padding: 25px 20px;
border-radius: 10px;
text-align: center;
background-color: whitesmoke;
}
/*end of menu styles*/
/*content styles**************************************************/
.content
{
padding: 40px 30px;
}
/*end of content styles/*
/*footer styles***************************************************/
.footer
{
position: fixed;
bottom: 0;
width: 100%;
background-color: whitesmoke;
padding: 5px 0;
}
.copyright
{
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 14px;
text-align: center;
color: teal;
}
.form-hidden
{
padding: 0;
margin: 0;
display: inline;
}
The csstable.dt.
:css
.table-wrapper
{
margin: 20px auto;
padding: 20px;
}
table
{
margin: 0 auto;
padding: 20px;
border-collapse: collapse;
}
table td, table th
{
border: 1px solid black;
margin: 0;
padding: 0 5px;
}
.no-border
{
border: 0;
}
The empdelete.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2.brown Are you sure you want to delete this record?
form.form-grid(method="post", action="delete_employee")
div.form-grid-column
| Department:<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade:<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address:<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password:<br />
div.form-grid-column-field ********
div.form-grid-column
| First name:<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name:<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone:<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city):<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City:<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province:<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code:<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture:<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="id", value="#{e._id}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") No
div.form-grid-column
input.form-grid-column-button(type="submit", value="Yes")
The empedit.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Edit employee details
form.form-grid(method="post", action="edit_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#depid.form-grid-column-input(name="e_deprt", value="#{e.deprt}")
- foreach(dep; deps)
- if(dep == e.deprt)
option(value="#{dep}", selected) #{dep}
- else
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", value="#{e.paygd}")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", value="#{e.email}")
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", value="#{e.pword}")
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", value="#{e.fname}")
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", value="#{e.lname}")
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", value="#{e.phone}")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", value="#{e.street}")
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", value="#{e.city}")
div.form-grid-column Province<br />
select#province.form-grid-column-
input(name="e_province", value="#{e.province}")
- foreach(p; provs)
- if(p[0] == e.province)
option(value="#{p[0]}", selected) #{p[1]}
- else
option(value="#{p[0]}") #{p[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", value="#{e.postcode}")
div.form-grid-column ID Picture:<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo", value="#{e.photo}")
input(type="hidden", name="e__id", value="#{e._id}")
div.form-grid-column
br
a(href="list_employees")
button.form-grid-column-button(type="button") Cancel
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}
The emplist.dt.
extends layout
block maincontent
include csstable.dt
div.table-wrapper
table
tr
td.no-border(colspan="11")
- if(error)
span.error #{error}<br /><br />
tr
th Department
th First name
th Last name
th Phone number
th Email address
th Salary grade
th Street address
th City
th Province
th PostCode
th Action
- foreach(e; emps)
tr
td #{e.deprt}
td #{e.fname}
td #{e.lname}
td #{e.phone}
td #{e.email}
td #{e.paygd}
td #{e.street}
td #{e.city}
td #{e.province}
td #{e.postcode}
td
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/pen.ico")
|
form.form-hidden(method="get", action="delete_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/trash.ico")
|
The empnew.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 New employee details
form.form-grid(method="post", action="new_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#deprt.form-grid-column-input(name="e_deprt")
- foreach(dep; deps)
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", placeholder="Salary grade")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", placeholder="email@address.com", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", placeholder="password", required)
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", placeholder="First name", required)
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", placeholder="Last name", required)
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", placeholder="Phone number")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", placeholder="Street address", required)
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", placeholder="City", required)
div.form-grid-column Province<br />
select#province.form-grid-column-input(name="e_province")
- foreach(prov; provs)
option(value="#{prov[0]}") #{prov[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", placeholder="A1A 1A1")
div.form-grid-column ID Picture<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo")
input(type="hidden", name="e__id", value="123456789012345678901234")
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}
The empshow.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Employee details
form.form-grid(method="get", action="edit_employee")
div.form-grid-column
| Department<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password<br />
div.form-grid-column-field ********
div.form-grid-column
| First name<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city)<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="id", value="#{e._id}")
input(type="hidden", name="e_photo", value="#{e.photo}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") Close
div.form-grid-column
input.form-grid-column-button(type="submit", value="Edit")
The index.dt.
extends layout
block maincontent
h2 The Lorem Ipsum Company
div
i (This text came from <a href="https://www.lipsum.com/" target="_blank" rel="
noopener noreferrer">this site.)</a><br /><br />
div.
Where does the Lorem Ipsum text come from?<br /><br />
Contrary to popular belief, Lorem Ipsum is not simply random text.
It has roots in a piece of classical Latin literature from 45 BC,
making it over 2000 years old. Richard McClintock, a Latin
professor at Hampden-Sydney College in Virginia, looked up one of
the more obscure Latin words, consectetur, from a Lorem Ipsum
passage, and going through the cites of the word in classical
literature, discovered the undoubtable source. Lorem Ipsum comes
from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et
Malorum" (The Extremes of Good and Evil) by Cicero, written in 45
BC. This book is a treatise on the theory of ethics, very popular
during the Renaissance. The first line of Lorem Ipsum, "Lorem
ipsum dolor sit amet..", comes from a line in section 1.10.32.
<br /><br /><br />
Why do we use it?<br /><br />
It is a long established fact that a reader will be distracted by
the readable content of a page when looking at its layout. The
point of using Lorem Ipsum is that it has a more-or-less normal
distribution of letters, as opposed to using 'Content here,
content here', making it look like readable English. Many desktop
publishing packages and web page editors now use Lorem Ipsum as
their default model text, and a search for 'lorem ipsum' will
uncover many web sites still in their infancy. Various versions
have evolved over the years, sometimes by accident, sometimes on
purpose (injected humour and the like).
<br /><br /><br />
The login.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Please log in first
form.form-grid(method="post", action="login")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="email", placeholder="email", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="password", placeholder="password", required)
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}<br /><br />
After typing in the SQL statements to create the database and the tables, click on the
lightning icon to execute.
C:\vibeprojects\ipsum>
Basic mysql-native dub app does not compile with dmd 2.095.0 · Issue #224 · mysql-
d/mysql-native (github.com)
I won’t be making a much explanations since they were mostly covered in the MongoDB
version.
Preparing to access a MySQL database
Again, we are presuming that this project will only be deployed in an intranet setting and
only very few people will have access and a very rare chance of more than one person
using the app at the same time.
To preserve the project as it is, let us create a new project named ipsum.
C:\vibeprojects>cd ipsum
First, add the mysql-native library to the list of dependencies of our project. As of now,
this is the dub.json file in the root directory of the project.
{
"authors": [
"rey"
],
"copyright": "Copyright © 2021, rey",
"dependencies": {
"vibe-d": "~>0.9"
},
"description": "A simple vibe.d server application.",
"license": "proprietary",
"name": "ipsum"
}
An alternative and safer way is to use dub to add the mysql-native library to our project.
module empmysql;
import std.array;
import std.conv;
import mysql;
struct Employee
{
int _id;
string deprt;
string paygd;
string email;
string pword;
string fname;
string lname;
string phone;
string photo;
string street;
string city;
string province;
string postcode;
}
struct Admin
{
string email;
string pword;
}
class EmployeeModel
{
private Connection conn;
this()
{
string c = "host=localhost;port=3306;user=root;pwd=password;db=empdb";
conn = new Connection(c);
scope(exit) conn.close;
}
Employee[] getEmployees()
{
Employee[] emps;
Row[] rows = conn.query("select * from employees").array;
if(rows.length) return prepEmployees(rows);
return emps;
}
ulong editEmployee(Employee e)
{
string sql = "update employees set
deprt=?, paygd=?, email=?, pword=?,
fname=?, lname=?, phone=?, photo=?,
street=?, city=?, province=?, postcode=?
where _id=?";
Prepared stmt = conn.prepare(sql);
stmt.setArgs
(
to!string(e.deprt),
to!string(e.paygd),
to!string(e.email),
to!string(e.pword),
to!string(e.fname),
to!string(e.lname),
to!string(e.phone),
to!string(e.photo),
to!string(e.street),
to!string(e.city),
to!string(e.province),
to!string(e.postcode),
to!int(to!string(e._id))
);
return conn.exec(stmt);
}
Admin[] getAdmins()
{
Admin[] admins;
Row[] rows = conn.query("select email, pword from admins").array;
foreach (Row row; rows)
{
Admin a;
a.email = to!string(row[0]);
a.pword = to!string(row[1]);
admins ~= a;
}
return admins;
}
In prepEmployee() function, you should make the order of fields in your code conform to
the order of fields in your MySQL table.
Sometimes, MySQL does not save the fields in the same order that you created the table.
module empcontrol;
import vibe.vibe;
import vibe.http.auth.digest_auth;
import std.file;
import std.path;
import std.algorithm;
import empmysql;
struct User
{
bool loggedIn;
string email;
}
class EmployeeController
{
private EmployeeModel empModel;
private SessionVar!(User, "user") m_user;
string realm = "Lorem Ipsum Company";
string[] deps =
[
"Management and Admin",
"Accounting and Finance",
"Production",
"Maintenance",
"Shipping and Receiving",
"Purchasing and Supplies",
"IT Services",
"Human Resources",
"Marketing"
];
string[][] provs =
[
["AB", "Alberta"],
["BC", "British Columbia"],
["MB", "Manitoba"],
["NB", "New Brunswick"],
["NL", "Newfoundland and Labrador"],
["NS", "Nova Scotia"],
["NT", "Northwest Territories"],
["NU", "Nunavut"],
["ON", "Ontario"],
["PE", "Prince Edward Island"],
["QC", "Quebec"],
["SK", "Saskatchewan"],
["YT", "Yukon Territory"]
];
this()
{
empModel = new EmployeeModel;
}
void index()
{
m_user = User.init;
terminateSession;
render!("index.dt");
}
@auth
void getNewEmployee(string _authUser, string _error = null)
{
string error = _error;
render!("empnew.dt", deps, provs, error);
}
@errorDisplay!getNewEmployee
@auth
void postNewEmployee(string _authUser, Employee e)
{
string photopath = "No photo submitted";
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.addEmployee(e);
redirect("list_employees");
}
@auth
void getListEmployees(string _authUser, string _error = null)
{
string error = _error;
Employee[] emps = empModel.getEmployees;
render!("emplist.dt", emps, error);
}
@auth
void getEditEmployee(string _authUser, int id, string _error = null)
{
string error = _error;
Employee e = empModel.getEmployee(id);
render!("empedit.dt", e, deps, provs, error);
}
@errorDisplay!getEditEmployee
@auth
void postEditEmployee(string _authUser, Employee e)
{
string photopath = e.photo;
auto pic = "picture" in request.files;
if(pic !is null)
{
string ext = extension(pic.filename.name);
string[] exts = [".jpg", ".jpeg", ".png", ".gif"];
if(canFind(exts, ext))
{
photopath = "uploads/photos/" ~ e.fname ~ "_" ~ e.lname ~ ext;
string dir = "./public/uploads/photos/";
mkdirRecurse(dir);
string fullpath = dir ~ e.fname ~ "_" ~ e.lname ~ ext;
try moveFile(pic.tempPath, NativePath(fullpath));
catch (Exception ex) copyFile(pic.tempPath, NativePath(fullpath));
}
}
e.photo = photopath;
if(e.phone.length == 0) e.phone = "(123) 456 7890";
if(e.paygd.length == 0) e.paygd = "none yet";
if(e.postcode.length == 0) e.postcode = "A1A 1A1";
e.pword = createDigestPassword(realm, e.email, e.pword);
empModel.editEmployee(e);
redirect("list_employees");
}
@auth
void getDeleteEmployee(string _authUser, int id)
{
Employee e = empModel.getEmployee(id);
render!("empdelete.dt", e);
}
@auth
void postDeleteEmployee(string _authUser, int id)
{
empModel.deleteEmployee(id);
redirect("list_employees");
}
@errorDisplay!getListEmployees
@auth
void getShowEmployee(string _authUser, string fname, string lname)
{
Employee e = empModel.getEmployee(fname, lname);
enforce(e != Employee.init, "Employee not found!");
render!("empshow.dt", e);
}
@errorDisplay!getLogin
void postLogin(string email, string password)
{
auto pword = createDigestPassword(realm, email, password);
bool isAdmin = empModel.getAdmin(email, pword);
enforce(isAdmin, "Email and password combination not found!");
User user = m_user;
user.loggedIn = true;
user.email = email;
m_user = user;
redirect("list_employees");
}
void getEncryptPasswords()
{
Admin[] admins = empModel.getAdmins;
foreach (Admin a; admins)
{
auto password = createDigestPassword(realm, a.email, a.pword);
empModel.encryptAdminPassword(a.email, password);
}
redirect("list_employees");
}
The views\empdelete.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2.brown Are you sure you want to delete this record?
form.form-grid(method="post", action="delete_employee")
div.form-grid-column
| Department:<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade:<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address:<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password:<br />
div.form-grid-column-field ********
div.form-grid-column
| First name:<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name:<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone:<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city):<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City:<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province:<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code:<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture:<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="id", value="#{e._id}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") No
div.form-grid-column
input.form-grid-column-button(type="submit", value="Yes")
The views\empedit.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Edit employee details
form.form-grid(method="post", action="edit_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#depid.form-grid-column-input(name="e_deprt", value="#{e.deprt}")
- foreach(dep; deps)
- if(dep == e.deprt)
option(value="#{dep}", selected) #{dep}
- else
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", value="#{e.paygd}")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", value="#{e.email}")
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", value="#{e.pword}")
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", value="#{e.fname}")
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", value="#{e.lname}")
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", value="#{e.phone}")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", value="#{e.street}")
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", value="#{e.city}")
div.form-grid-column Province<br />
select#province.form-grid-column-
input(name="e_province", value="#{e.province}")
- foreach(p; provs)
- if(p[0] == e.province)
option(value="#{p[0]}", selected) #{p[1]}
- else
option(value="#{p[0]}") #{p[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", value="#{e.postcode}")
div.form-grid-column ID Picture:<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo", value="#{e.photo}")
input(type="hidden", name="e__id", value="#{e._id}")
input(type="hidden", name="id", value="#{e._id}")
div.form-grid-column
br
a(href="list_employees")
button.form-grid-column-button(type="button") Cancel
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}<br /><br />
The views\emplist.dt.
extends layout
block maincontent
include csstable.dt
div.table-wrapper
table
tr
td.no-border(colspan="11")
- if(error)
span.error #{error}<br /><br />
tr
th Department
th Salary grade
th First name
th Last name
th Phone number
th Email address
th Street address
th City
th Province
th PostCode
th Action
- foreach(e; emps)
tr
td #{e.deprt}
td #{e.paygd}
td #{e.fname}
td #{e.lname}
td #{e.phone}
td #{e.email}
td #{e.street}
td #{e.city}
td #{e.province}
td #{e.postcode}
td
form.form-hidden(method="get", action="edit_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/pen.ico")
|
form.form-hidden(method="get", action="delete_employee")
input(type="hidden", name="id", value="#{e._id}")
input(type="image", src="images/trash.ico")
|
The views\empnew.dt.
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 New employee details
form.form-grid(method="post", action="new_employee", enctype="multipart/form-
data")
div.form-grid-column Department<br />
select#deprt.form-grid-column-input(name="e_deprt")
- foreach(dep; deps)
option(value="#{dep}") #{dep}
div.form-grid-column Salary grade<br />
input.form-grid-column-
input(type="text", name="e_paygd", placeholder="Salary grade")
div.form-grid-column Email address<br />
input.form-grid-column-
input(type="email", name="e_email", placeholder="email@address.com", required)
div.form-grid-column Password<br />
input.form-grid-column-
input(type="password", name="e_pword", placeholder="password", required)
div.form-grid-column First name<br />
input.form-grid-column-
input(type="text", name="e_fname", placeholder="First name", required)
div.form-grid-column Last name<br />
input.form-grid-column-
input(type="text", name="e_lname", placeholder="Last name", required)
div.form-grid-column Phone<br />
input.form-grid-column-
input(type="text", name="e_phone", placeholder="Phone number")
div.form-grid-column Street address (without city)<br />
input.form-grid-column-
input(type="text", name="e_street", placeholder="Street address", required)
div.form-grid-column City<br />
input.form-grid-column-
input(type="text", name="e_city", placeholder="City", required)
div.form-grid-column Province<br />
select#province.form-grid-column-input(name="e_province")
- foreach(prov; provs)
option(value="#{prov[0]}") #{prov[1]}
div.form-grid-column Postal code<br />
input.form-grid-column-
input(type="text", name="e_postcode", placeholder="A1A 1A1")
div.form-grid-column ID Picture<br />
input.form-grid-column-input(type="file", name="picture")
input(type="hidden", name="e_photo")
input(type="hidden", name="e__id", value="0")
div.form-grid-column
br
input.form-grid-column-button(type="reset", value="Clear")
div.form-grid-column
br
input.form-grid-column-button(type="submit", value="Submit")
- if(error)
div.error #{error}
extends layout
block maincontent
include cssformgrid.dt
div.form-grid-wrapper
h2 Employee details
form.form-grid(method="get", action="edit_employee")
div.form-grid-column
| Department<br />
div.form-grid-column-field #{e.deprt}
div.form-grid-column
| Salary grade<br />
div.form-grid-column-field #{e.paygd}
div.form-grid-column
| Email address<br />
div.form-grid-column-field #{e.email}
div.form-grid-column
| Password<br />
div.form-grid-column-field ********
div.form-grid-column
| First name<br />
div.form-grid-column-field #{e.fname}
div.form-grid-column
| Last name<br />
div.form-grid-column-field #{e.lname}
div.form-grid-column
| Phone<br />
div.form-grid-column-field #{e.phone}
div.form-grid-column
| Street address (without city)<br />
div.form-grid-column-field #{e.street}
div.form-grid-column
| City<br />
div.form-grid-column-field #{e.city}
div.form-grid-column
| Province<br />
div.form-grid-column-field #{e.province}
div.form-grid-column
| Postal code<br />
div.form-grid-column-field #{e.postcode}
div.form-grid-column
| Profile picture<br />
img(src="#{e.photo}", height="100px")
input(type="hidden", name="id", value="#{e._id}")
input(type="hidden", name="e_photo", value="#{e.photo}")
div.form-grid-column
a(href="list_employees")
button.form-grid-column-button(type="button") Close
div.form-grid-column
input.form-grid-column-button(type="submit", value="Edit")
Compile, run and refresh the browser. If you see the same app that we did with
MongoDB, you did good.
We are done!
Hello, main() again!
D is a case-sensitive language, just like the other C-derived languages.
dub
C:\dprojects>cd hello
C:\dprojects\hello>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
hello ~master: building configuration "application"...
Linking...
Running .\hello.exe
Edit source/app.d to start your project.
C:\dprojects\hello>
import std.stdio;
void main()
{
writeln("Hello, world!");
}
C:\dprojects\hello>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
hello ~master: building configuration "application"...
Linking...
Running .\hello.exe
Hello, world!
C:\dprojects\hello>
There.
You see that the code is similar to C or C++. That’s because Walter Bright (later aided by
Andrei Alexandrescu, then a host of others) wanted to create a better C or C++ minus the
extra complexities of C++.
We need to import std.stdio because the writeln() function is in that library, along with
other input/output functions. Some of the I/O functions in std.stdio are
write
writeln
writef
writefln
readf
readln
stdin
stdout
The main() function is the entry point for D programs. Even if the project is composed of
several files, only one main() function is required as the entry point and it determines the
order of execution of the whole program.
import std.stdio;
C:\dprojects\hello>
import std.stdio;
import std.conv;
We have to import std.conv because the conversion functions are there, such as
to!string().
Of course we should be using curly brackets inside the loop to signify the block of code
following convention, but sometimes we are just lazy.
import std.stdio;
C:\dprojects\hello>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
hello ~master: building configuration "application"...
Linking...
Running .\hello.exe
0: .\hello.exe
We can also use the formatted way of printing to stdout by using writef() or writefln().
This time we don’t need the conversion function, so we don’t have to import std.conv.
import std.stdio;
C:\dprojects\hello>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
hello ~master: building configuration "application"...
Linking...
Running .\hello.exe
0: .\hello.exe
The main() function can also return a value, called the exit status, although a non-zero
value means an error occurred.
import std.stdio;
C:\dprojects\hello>dub
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
hello ~master: building configuration "application"...
Linking...
Running .\hello.exe
Hello, how are you?
C:\dprojects\hello>
import std.stdio;
This time, we indicated an int return value as well as an array of strings as parameter. We
did not use the argument args within the function though.
The writeln() and writefln() functions write to stdout, which is the screen. It is included in
the std.stdio module, that’s why we imported std.stdio.
Instead of using dub, we can simply call the DMD compiler directly if we are writing a
simple program that doesn’t need the added complexity of a project structure. To use the
DMD compiler, you should be in the same folder as the source file you want to compile.
C:\dprojects\hello>cd source
C:\dprojects\hello\source>dir
Volume in drive C has no label.
Volume Serial Number is D4B4-80DE
Directory of C:\dprojects\hello\source
C:\dprojects\hello\source>dmd app.d
C:\dprojects\hello\source>dir
Volume in drive C has no label.
Volume Serial Number is D4B4-80DE
Directory of C:\dprojects\hello\source
C:\dprojects\hello\source>app.exe
Hello, how are you?
C:\dprojects\hello\source>
D keywords
Here are the D keywords to avoid when declaring variables, constants and functions.
Special tokens
You must be familiar with this syntax if you came from the C-related languages.
int a = 0;
bool isReady = false;
double netIncome = grossIncome – deductions;
float price = itemCost + freight * duties;
To declare a variable, the data type comes first before the variable name.
int a = 0;
In this case, isReady is declared as a Boolean variable with an initial value of false.
Operations on variables are expressions. When you assign the result of an expression to
something, that becomes a statement. Yet simply declaring a variable or a user-defined
data type is already a statement. Go figure.
Since you are interested in Vibe.d, you must be familiar with these already.
= assignment operator
== equality operator
+ addition operator
* multiplication operator
- subtraction operator
/ division operator
>= relational (comparison) operator