MEVN stack experiment with Jupyter
In this notebook I want to play with Jupyter and show the steps of how to create a MEVN application from a notebook. Normally I would do this in the normal Linux terminal and a text editor, but since we can combine code, explanation and shell commands, I want to create a story in this notebook which hopefully will be of any help of people experimenting with full-stack development. I will create the application, use some simple Linux tricks and use Selenium to test the application.
Please note development like this is far from optimal, but I think it is very cool what you can achieve without leaving your notebook, knowing a bit of Docker, Linux and Python.
The original notebook can be found here.
Objective¶
Setup an application with the following elements
using
Notebook settings¶
%%html
<style>
img.logo{
height: 100px;
}
img.screenshot{
max-width:500px;
-webkit-filter: drop-shadow(5px 5px 5px #222);
filter: drop-shadow(2px 5px 5px #222);
margin: 50px auto;
}
</style>
Clean up the images from earlier runs.
rm *.png
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
!jupyter --version
!jupyter notebook --version
Python version¶
import platform
platform.python_version()
!docker version
!docker ps
pip3 install docker
import docker
docker_client = docker.from_env()
Available containers¶
for cntr in docker_client.containers.list():
print("name={} (id={})".format(cntr.name, cntr.id))
MongoDB¶
Check if the Mongo Docker container is running, otherwise, start the container.
mongo_running = False
for cntr in docker_client.containers.list():
if 'mongo' in cntr.attrs['Config']['Image']:
mongo_running = True
container = cntr
if mongo_running is False:
container = docker_client.containers.run("mongo:latest", name='mongo', ports={'27017': '27017'}, detach=True)
container
Verification that the Mongo container is running:
!docker ps | grep mongo
See Documentation to install PyMongo.
$ pip install pymongo
pip3 install pymongo
from pymongo import MongoClient
mongo_client = MongoClient('localhost', 27017)
Data gathering¶
Create the Scrapy pipeline to write the scraping results to MongoDB.
import pymongo
class MongoPipeline(object):
collection_name = 'games'
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db[self.collection_name].insert_one(dict(item))
return item
Retrieving data from https://www.nintendo.co.jp/ir/en/finance/software/index.html with the following markup:
<ul class="sales_layout">
<li class="sales_layout_list">
<div class="ta_l">
<p class="sales_title">The Legend of Zelda:<br> Breath of the Wild</p>
<p class="sales_value"><span>4.70</span> million pcs.</p>
</div>
<div class="ta_r">
<p>
<img src="./img/data_switch_001.png" alt="" width="110" height="" class="sales_product1">
</p>
</div>
</li>
</ul>
Using Scrapy we can gather the data in a convenient way.
pip3 install scrapy
import logging
import scrapy
from scrapy.crawler import CrawlerProcess
import re
class ConsoleSpider(scrapy.Spider):
name = "games"
start_urls = [
"https://www.nintendo.co.jp/ir/en/finance/software/index.html",
"https://www.nintendo.co.jp/ir/en/finance/software/wiiu.html",
"https://www.nintendo.co.jp/ir/en/finance/software/3ds.html",
"https://www.nintendo.co.jp/ir/en/finance/software/wii.html",
"https://www.nintendo.co.jp/ir/en/finance/software/ds.html"
]
custom_settings = {
'LOG_LEVEL': logging.CRITICAL,
'DOWNLOAD_DELAY': .25,
'RANDOMIZE_DOWNLOAD_DELAY': True,
'ITEM_PIPELINES': {
'__main__.MongoPipeline': 300,
},
'MONGO_URI': 'mongodb://localhost:27017',
'MONGO_DATABASE': 'nintendo'
}
def parse(self, response):
for cons in response.css('li.sales_layout_list'):
yield {
'console': response.css('div.tab div.tabInner span::text').extract_first(),
'name': cons.css('p.sales_title::text').extract_first().strip(),
'image': 'https://www.nintendo.co.jp/ir/en/finance/software' + cons.css('p img::attr(src)').extract_first()[1:],
'sales': cons.css('p.sales_value span::text').extract()[0].strip()
}
process = CrawlerProcess({
'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
})
process.crawl(ConsoleSpider)
process.start()
Verify the Nintendo database is created¶
if 'nintendo' in mongo_client.database_names():
print('Database found!')
Verify the games collection is created inside the Nintendo database¶
db = mongo_client['nintendo']
if 'games' in db.collection_names():
print('Collection found!')
Retrieve the games from the collection¶
games = list(db['games'].find({}))
print("Found {} games".format(len(games)))
!rm -rf images/*
import re
updated_games = []
for gameindex, game in enumerate(games):
image = game['image']
image_short = image.split('/')[-1]
!wget --no-check-certificate $image -P images/
game['image'] = 'images/' + image_short
game.pop('_id', None)
updated_games.append(game)
Verify the downloaded images¶
Run the ls images
to show the available images and save the list to lsimages
.
lsimages = !ls images
lsimages
Show the first image¶
from IPython.display import Image
from IPython.core.display import HTML
Image(url='images/'+lsimages.list[0])
Delete the old games from the database¶
result = db.games.delete_many({})
Insert the updated games¶
result = db.games.insert_many(updated_games)
!node --version
!npm --version
!rm -rf server
!mkdir server && cd server && npm init -y && npm install express-generator --save
Check ExpressJS version¶
!cd server && npm express --version
Create the scaffold for the server¶
Note that we use npx
instead of npm
because we run NPM from a local folder.
!cd server && npx express -y --force --git --view ejs .
ls server/
!cat server/package.json
Install dependencies¶
!cd server && npm install
Run the app¶
import subprocess
proc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
proc.pid
Request the ExpressJS app¶
import requests
import time
time.sleep(5)
resp = requests.get('http://localhost:3000')
resp.status_code
Stop the ExpressJS app¶
Use lsof
to find the process that uses port 3000 and kill it.
process=!lsof -i:3000 -t
expressid = int(process[0])
expressid
!kill -9 $expressid
Check if the server is down¶
try:
resp = requests.get('http://localhost:3000')
except:
print("Server is down!")
Install package¶
!cd server && npm install --save mongoose
Check installation¶
!cat server/package.json | grep mongoose
Add the Mongoose connection¶
Add the following to the top of server/app.js
.
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/nintendo');
var db = mongoose.connection;
db.on("error", console.error.bind(console, "Connection error"));
db.once("open", function(callback){
console.log("Connection successful")
});
%%file server/app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
// Mongoose connection
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/nintendo');
var db = mongoose.connection;
db.on("error", console.error.bind(console, "Connection error"));
db.once("open", function(callback){
console.log("Connection successful")
});
var index = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
!head -n 20 server/app.js
Check if MongoDB initializes¶
import subprocess
proc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
import time
time.sleep(5)
for line in proc.stdout:
print(str(line))
if 'Connection successful' in str(line):
print("Success!")
break
process=!lsof -i:3000 -t
expressid = int(process[0])
!kill -9 $expressid
Create new model¶
Create the Mongoose model for the games that we gathered in the earlier step.
!mkdir server/models
%%file server/models/game.js
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var GameSchema = new Schema({
console: {
type: String
},
name: {
type: String
},
image: {
type: String
},
sales: {
type: String
}
});
module.exports = mongoose.model("Game", GameSchema);
Adding new route¶
Create the route to access the data of the games. In the scaffold we already have the index.js
and users.js
, so lets create the games.js
to setup the routes for the new pages.
Existing routes¶
!cat server/routes/index.js
!cat server/routes/users.js
Add the route requirement¶
Add the following to server/app.js
:
...
var consoles = require('./routes/games.js');
...
app.use('/games', games);
...
With sed
we can insert text on a certain position in a file.
!sed -i "18i var games = require('./routes/games');" server/app.js
Enable the routes in the app:
!sed -i "36i app.use('/games', games);" server/app.js
and create the route file server/routes/games.js
:
%%file server/routes/games.js
var express = require('express');
var router = express.Router();
var Game = require("../models/game");
router.get('/', (req, res) => {
Game.find({}, '', function (error, games){
if (error) { game.error(error); }
res.send({
games: games
})
}).sort({_id:-1})
})
module.exports = router;
Start the server and verify the new route which should return a JSON object.
import json
import requests
import time
proc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(5)
resp = requests.get('http://localhost:3000/games').json()
print(json.dumps(resp, indent=4))
Lets grab the ID of the first game of the data to verify the route in the next step.
game_id = resp['games'][0]['_id']
game_id
Kill the application again.
process=!fuser 3000/tcp | awk '{print $1}'
expressid = int(process[1])
!kill -9 $expressid
Add another route to the games.js
to get information for a single game.
router.get('/:id', (req, res) => {
var db = req.db;
Game.findById(req.params.id, '', function (error, game) {
if (error) { console.error(error); }
res.send(game)
})
})
%%file server/routes/games.js
var express = require('express');
var router = express.Router();
var Game = require("../models/game");
router.get('/', (req, res) => {
Game.find({}, '', function (error, games){
if (error) { game.error(error); }
res.send({
games: games
})
}).sort({_id:-1})
})
router.get('/:id', (req, res) => {
var db = req.db;
Game.findById(req.params.id, '', function (error, game) {
if (error) { console.error(error); }
res.send(game)
})
})
module.exports = router;
Verify the game detail route¶
As a final step to verify the API we check if we can get the detailed information for the game ID we saved in the previous step.
import time
proc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(5)
resp = requests.get('http://localhost:3000/games/'+game_id).json()
print(json.dumps(resp, indent=4))
process=!fuser 3000/tcp | awk '{print $1}'
expressid = int(process[1])
!kill -9 $expressid
Conclusion¶
For the back-end we have created an API using Mongoose and ExpressJS with the following two routes:
- All games
- Game detail
!rm -rf client
!mkdir client && cd client && npm init -y && npm install vue-cli --save
ls -la client
Check version of Vue¶
!cd client && npm vue --version
Create the scaffold¶
To create the scaffold we will use the vue-cli
. However, this requires us to give direct input to the command line which is tricky because we need to answer different questions when creating the scaffold using the tool. The easiest way to automate this task is to use the Linux tool Expect. Make sure the tool is installed on your system.
$ sudo apt-get install expect -y
Note: if you do not want to use this trick, you can also use the external terminal and run
$ vue init webpack
in the client directory or simply clone the repository to your local file system.
The following script contains the answers for the prompts created by the vue init
.
%%file client/init_expect_script.sh
#!/usr/bin/expect -f
spawn npx vue init webpack
expect "Generate project in current directory?" { send "Y\r" }
expect "Project name"
send "vueclient\r"
expect "Project description"
send "An experiment with Jupyter and MEVN\r"
expect "Author"
send "Jupyter\r\n"
expect "Vue build"
send "\r\n"
expect "Install vue-router?"
send "Y\r"
expect "Use ESLint to lint your code?"
send "Y\r"
expect "Pick an ESLint preset"
send "\r\n"
expect "Set up unit tests"
send "Y\r"
expect "Pick a test runner"
send "\r\n"
expect "Setup e2e tests with Nightwatch?"
send "Y\r"
expect "Should we run `npm install` for you after the project has been created? (recommended)"
send "\r\n"
interact
!chmod +x client/init_expect_script.sh
!cd client && ./init_expect_script.sh
ls client
!cd client && npm install [email protected] --save-dev
import time
proc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
Setup Selenium¶
First create a Docker instance that runs Selenium to avoid the cumbersome installation on the local Linux machine.
selenium_running = False
for cntr in docker_client.containers.list():
if 'selenium' in cntr.attrs['Config']['Image']:
selenium_running = True
container = cntr
if selenium_running is False:
container = docker_client.containers.run("selenium/standalone-chrome:latest", name='selenium', ports={'4444': '4444'}, detach=True)
time.sleep(10)
Install Selenium with pip
pip install selenium
!pip3 install selenium
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
driver = webdriver.Remote("http://localhost:4444/wd/hub", DesiredCapabilities.CHROME)
driver.get('http://prod.jitsejan.com:8080')
driver.save_screenshot('vue_frontpage.png')
from IPython.display import Image
Image("vue_frontpage.png")
Kill the process on port 8080 after verifying the front page.
process=!fuser 8080/tcp | awk '{print $1}'
vueid = int(process[1])
!kill -9 $vueid
As we can see from the screenshot, we cannot access the page because webpack
has changed some configuration. In order to circument this we need to add the host to the script in package.json
and change
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
to
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js --host 0.0.0.0",
which can be done with sed
:
!sed -i '/dev.*--inline/s/conf.js/conf.js --host 0.0.0.0/' client/package.json
cat client/package.json | grep dev
Additionally we need to disable the host check in build/webpack.dev.conf.js
by adding
disableHostCheck: true
to the devServer
.
!sed -i "26i\ \ \ \ disableHostCheck: true," client/build/webpack.dev.conf.js
!grep -C3 disableHostCheck client/build/webpack.dev.conf.js --color=auto
Retry¶
proc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
driver = webdriver.Remote("http://localhost:4444/wd/hub", DesiredCapabilities.CHROME)
driver.get('http://prod.jitsejan.com:8080')
driver.get_screenshot_as_file('vue_front.png')
Image("vue_front.png")
process=!fuser 8080/tcp | awk '{print $1}'
vueid = int(process[1])
!kill -9 $vueid
Create games page¶
ls client/src/components/
Create the component¶
Add the Vue template to the Games component.
<template>
<div class="games">
This page will list all the games.
</div>
</template>
<script>
export default {
name: 'Games',
data () {
return {}
}
}
</script>
%%file client/src/components/Games.vue
<template>
<div class="games">
This page will list all the games.
</div>
</template>
<script>
export default {
name: 'Games',
data () {
return {}
}
}
</script>
ls client/src/components/
Create the route¶
cat client/src/router/index.js
Add the import of the Games component to the router file.
import Games from '@/components/Games'
and add the path to the games page indicating to which component the page will link.
...
{
path: '/games',
name: 'Games',
component: Games
}
...
%%file client/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Games from '@/components/Games'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/games',
name: 'Games',
component: Games
}
]
})
Verify the new route¶
proc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
driver = webdriver.Remote("http://localhost:4444/wd/hub", DesiredCapabilities.CHROME)
driver.get('http://prod.jitsejan.com:8080/ui/#/games')
driver.get_screenshot_as_file('new_route.png')
process=!fuser 8080/tcp | awk '{print $1}'
vueid = int(process[1])
!kill -9 $vueid
Image("new_route.png")
!cd client && npm install --save axios
Setup connection with back-end¶
Create a services
folder that will contain the API Javascript files.
mkdir -p client/src/services
%%file client/src/services/api.js
import axios from 'axios'
export default() => {
return axios.create({
baseURL: `http://prod.jitsejan.com:3000`
})
}
Create service to retrieve the games¶
%%file client/src/services/GamesService.js
import api from '@/services/api'
export default {
fetchGames () {
return api().get('games')
}
}
Add the service to the Games component¶
This component shows all the games available in the database. Note that this is the file that should be updated if you would like a more fancy layout, for example by adding Bootstrap or other frameworks.
<template>
<div class="games">
<div class="games-div" v-for="game in games" :key="game._id">
<p>
<span><b>{{ game.name }}</b></span><br />
<span>{{ game.console }}</span><br />
<span>{{ game.sales }}</span>
<a :href="'/ui/#/games/' + game._id">Details</a>
</p>
</div>
</div>
</template>
<script>
import GamesService from '@/services/GamesService'
export default {
name: 'Games',
data () {
return {
games: []
}
},
mounted () {
this.getGames()
},
methods: {
async getGames () {
const response = await GamesService.fetchGames()
this.games = response.data.games
}
}
}
</script>
%%file client/src/components/Games.vue
<template>
<div class="games">
<div class="games-div" v-for="game in games" :key="game._id">
<p>
<span><b>{{ game.name }}</b></span><br />
<span>{{ game.console }}</span><br />
<span>{{ game.sales }}</span>
<a :href="'/ui/#/games/' + game._id">Details</a>
</p>
</div>
</div>
</template>
<script>
import GamesService from '@/services/GamesService'
export default {
name: 'Games',
data () {
return {
games: []
}
},
mounted () {
this.getGames()
},
methods: {
async getGames () {
const response = await GamesService.fetchGames()
this.games = response.data.games
}
}
}
</script>
Start both the Express back-end and the Vue front-end to verify if the data from the API is retrieved with the updated component.
Small trick: by setting the logging preferences when starting the webdriver with Selenium, we can actually retrieve the logs you would normally see with the developer tools in the browser.
vueproc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
expressproc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
desired = DesiredCapabilities.CHROME
desired ['loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Remote("http://localhost:4444/wd/hub", desired_capabilities=desired)
driver.get('http://prod.jitsejan.com:8080/ui/#/games')
driver.get_screenshot_as_file('new_route_with_data.png')
vueprocess=!fuser 8080/tcp | awk '{print $1}'
vueid = int(vueprocess[1])
!kill -9 $vueid
expressprocess=!fuser 3000/tcp | awk '{print $1}'
expressid = int(expressprocess[1])
!kill -9 $expressid
logs = driver.get_log('browser')
logs
Image("new_route_with_data.png")
As we can see in the logs, we have an issue accessing the back-end because of the Access-Control-Allow-Origin limitation. To circumvent this, we need to make sure that we are allowed to access the API; hece install the package Cors and enable it in the Express application.
!cd server && npm install --save cors
Add the requirement for the cors package and enable it in the Express app by adding the following to server/app.js
.
...
var cors = require('cors')
...
app.use(cors())
...
%%file server/app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
// Mongoose connection
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/nintendo');
var db = mongoose.connection;
db.on("error", console.error.bind(console, "Connection error"));
db.once("openUri", function(callback){
console.log("Connection successful")
});
var cors = require("cors")
var index = require('./routes/index');
var users = require('./routes/users');
var games = require('./routes/games');
var app = express();
app.use(cors());
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
app.use('/users', users);
app.use('/games', games);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
Now we are connected and allowed to retrieve the data. We use Selenium and wait for the games-div
to appear.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
vueproc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
expressproc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
desired = DesiredCapabilities.CHROME
desired ['loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Remote("http://localhost:4444/wd/hub", desired_capabilities=desired)
driver.get('http://prod.jitsejan.com:8080/ui/#/games')
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.CLASS_NAME, 'games-div')))
driver.refresh()
driver.save_screenshot('allgames.png')
Image("allgames.png")
Clean up¶
driver.quit()
vueprocess=!fuser 8080/tcp | awk '{print $1}'
vueid = int(vueprocess[1])
!kill -9 $vueid
expressprocess=!fuser 3000/tcp | awk '{print $1}'
expressid = int(expressprocess[1])
!kill -9 $expressid
Create detail page for the game¶
Create the component and connect to the API by importing the service and execute the promise. Retrieving the data for a game can also be solved by extending the GamesServices.js
we created earlier by adding another fetch function, but since every problem can be solved in different ways, I chose to use the method as shown in the following file.
<template>
<div class="games-detail">
<h1>Details for {{ data.name }}</h1>
<img :src="'/static/' + data.image" /><br/>
Console: {{ data.console }}<br/>
Sales: {{ data.sales}} million<br/>
</div>
</template>
<script>
import api from '@/services/api'
export default {
name: 'GamesDetail',
data () {
return {
data: ''
}
},
created () {
api().get('games/' + this.$route.params.id)
.then(response => {
this.data = response.data
})
.catch(e => {
this.errors.push(e)
})
}
}
</script>
%%file client/src/components/GamesDetail.vue
<template>
<div class="games-detail">
<h1>Details for {{ data.name }}</h1>
<img :src="'/static/' + data.image" /><br/>
Console: {{ data.console }}<br/>
Sales: {{ data.sales}} million<br/>
</div>
</template>
<script>
import api from '@/services/api'
export default {
name: 'GamesDetail',
data () {
return {
data: ''
}
},
created () {
api().get('games/' + this.$route.params.id)
.then(response => {
this.data = response.data
})
.catch(e => {
this.errors.push(e)
})
}
}
</script>
Add the route for the detail page by adding the following to client/src/router/index.js
:
...
import GamesDetail from '@/components/GamesDetail'
...
{
path: '/games/:id',
name: 'GamesDetail',
component: GamesDetail
}
%%file client/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Games from '@/components/Games'
import GamesDetail from '@/components/GamesDetail'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/games',
name: 'Games',
component: Games
},
{
path: '/games/:id',
name: 'GamesDetail',
component: GamesDetail
}
]
})
Start the processes
vueproc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
expressproc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
Verify the detail page for a given ID.
desired = DesiredCapabilities.CHROME
desired ['loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Remote("http://localhost:4444/wd/hub", desired_capabilities=desired)
driver.get('http://prod.jitsejan.com:8080/ui/#/games/'+game_id)
driver.save_screenshot('gamedetail.png')
Image("gamedetail.png")
Note: not all images from the source are working properly, but that could easily be fixed by improving the webscraper and verifying the images.
vueprocess=!fuser 8080/tcp | awk '{print $1}'
vueid = int(vueprocess[1])
!kill -9 $vueid
expressprocess=!fuser 3000/tcp | awk '{print $1}'
expressid = int(expressprocess[1])
!kill -9 $expressid
Finalize¶
To wrap this notebook up, lets update the generic App.vue
to make it easier to navigate between the pages by adding a menu to client/src/App.vue
. Copy the following text to the client/src/App.vue
.
<template>
<div id="app">
<router-link :to="{ name: 'HelloWorld' }">Home</router-link>
<router-link :to="{ name: 'Games'}">Games</router-link>
<router-view/>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
%%file client/src/App.vue
<template>
<div id="app">
<router-link :to="{ name: 'HelloWorld' }">Home</router-link>
<router-link :to="{ name: 'Games'}">Games</router-link>
<router-view/>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
vueproc = subprocess.Popen("cd client && npm run dev &", shell=True, stdout=subprocess.PIPE)
expressproc = subprocess.Popen("cd server && npm start &", shell=True, stdout=subprocess.PIPE)
time.sleep(10)
desired = DesiredCapabilities.CHROME
desired ['loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Remote("http://localhost:4444/wd/hub", desired_capabilities=desired)
driver.get('http://prod.jitsejan.com:8080')
driver.save_screenshot('menuadded.png')
Image("menuadded.png")
Clean up¶
%%bash
rm -rf client
rm -rf server
rm -rf iamges
Future work¶
In this example application I did not use any fancy styling or advanced HTML structures to make the website more appealing. Additionally, the API is only used to retrieve data, while obviously it could also used to create data, update it or delete it (CRUD), but for the scope of this notebook I kept it to a minimum. As a third improvement you could choose to put everything of this notebook in one Docker compose file, where MongoDB, Selenium, ExpressJS and VueJS are all containerized, but since I am not planning to port this application to another server, it is fine to simply install the files locally and remove them again after the notebook has finished.