More big reworking for the appci

This commit is contained in:
Alexandre Aubin 2017-11-26 20:48:26 +01:00
parent 38c36a0361
commit a2766028df
14 changed files with 346 additions and 394 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
*.json
*.pyc
www
token
pullrequests/data/

26
appci/common.py Normal file
View file

@ -0,0 +1,26 @@
tests = [ "Package linter",
"Installation",
"Deleting",
"Upgrade",
"Backup",
"Restore",
"Change URL",
"Installation in a sub path",
"Deleting from a sub path",
"Installation on the root",
"Deleting from root",
"Installation in private mode",
"Installation in public mode",
"Multi-instance installations",
"Malformed path",
"Port already used" ]
ci_branches = [ ("stable", "Stable (x86)"),
("arm", "Stable (ARM)"),
("testing", "Testing"),
("unstable", "Unstable") ]

View file

@ -1,43 +0,0 @@
#!/bin/bash
python fetchlist.py | sort > list_apps
while read APP;
do
APPNAME=$(echo $APP | awk '{print $1}')
echo $APPNAME
wget -q -O data/$APPNAME "https://ci-apps.yunohost.org/jenkins/job/$APP/lastBuild/consoleText" --prefer-family=IPv4
TESTS_RESULTS=""
while read TESTNAME
do
RESULTS=$(grep "^$TESTNAME:" data/$APPNAME)
if echo $RESULTS | grep -q "FAIL"
then
TESTS_RESULTS="${TESTS_RESULTS}0"
elif echo $RESULTS | grep -q "SUCCESS"
then
TESTS_RESULTS="${TESTS_RESULTS}1"
else
TESTS_RESULTS="${TESTS_RESULTS}X"
fi
done < list_tests
# Get the level of this application and the value of each level
LEVELS=$(grep -A10 'Level of this application' data/$APPNAME \
| tail -n11 \
| sed 's/.*: \| (.*//g' \
| sed 's@N/A@X@g' \
| tr -d '\n')
# LEVELS=$(grep -A10 'Level of this application' data/$APPNAME \
# | tail -n 11 \
# | sed -e 's@N/A@X@g' -e 's/ Level //g' -e 's/Level of this application//g' \
# | awk '{print $2}' \
# | tr -d '\n')
echo $TESTS_RESULTS > data/$APPNAME
echo $LEVELS >> data/$APPNAME
done < list_apps

View file

@ -1,48 +1,18 @@
#!/usr/bin/python3
#!/usr/bin/python2.7
import os
import json
import glob
from publish_apps import main as publish_apps
from publish_branches import main as publish_branches
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
def main():
for link in glob.glob("../www/integration/*.svg"):
os.unlink(link);
os.symlink("%s/badges/unknown.svg" % os.getcwd(),
"../www/integration/unknown.svg")
###############################################################################
output_dir = "../www/"
template_path = os.path.join(output_dir,"template_appci.html")
output_path = os.path.join(output_dir,"appci.html")
summary_path = os.path.join("./", "summary.json")
###############################################################################
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme)))
def shell_to_html(shell):
return conv.convert(shell, False)
###############################################################################
publish_branches()
publish_apps()
if __name__ == '__main__':
main()
# Fetch the list of all reports, sorted in reverse-chronological order
#summary = json.load(open(summary_path))
summary = {}
summary["testnames"] = open("list_tests").read().strip().split('\n')
summary["apps"] = json.loads(open("apps.json").read())
# Generate the output using the template
template = open(template_path, "r").read()
t = Template(template)
result = t.render(data=summary, convert=shell_to_html, shell_css=shell_css)
open(output_path, "w").write(result)
print("Done.")

View file

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python2.7
import os
import json
@ -7,34 +7,12 @@ import glob
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
from common import tests, ci_branches
###############################################################################
output_dir = "../www/"
template_path = os.path.join(output_dir,"template_appci_perapp.html")
tests = [ "Package linter",
"Installation",
"Deleting",
"Upgrade",
"Backup",
"Restore",
"Change URL",
"Installation in a sub path",
"Deleting from a sub path",
"Installation on the root",
"Deleting from root",
"Installation in private mode",
"Installation in public mode",
"Multi-instance installations",
"Malformed path",
"Port already used" ]
ci_branches = [ ("stable", "Stable (x86)"),
("arm", "Stable (ARM)"),
("testing", "Testing"),
("unstable", "Unstable") ]
template_path = "./templates/app.html"
###############################################################################
@ -46,7 +24,7 @@ def shell_to_html(shell):
###############################################################################
if __name__ == '__main__':
def main():
# Load the template
template = open(template_path, "r").read()
@ -56,19 +34,28 @@ if __name__ == '__main__':
for app in apps:
print app
results = json.loads(open("data/" + app).read())
# Meh
try:
level = "level" + str(int(results["stable"]["level"]))
except:
level = "unknown"
os.symlink("%s/badges/%s.svg" % (os.getcwd(), level),
"../www/integration/%s.svg" % app)
data = {
"appname": app,
"ci_branches": ci_branches,
"tests": tests,
"results": json.loads(open("data/" + app).read()),
"results": results,
"result_to_class": { None:"unknown", False:"danger", True:"success" }
}
# Generate the output using the template
result = t.render(data=data, convert=shell_to_html, shell_css=shell_css)
output_path = os.path.join(output_dir,"ciperapp", "%s.html" % app)
output_path = os.path.join(output_dir, "appci", "app", "%s.html" % app)
open(output_path, "w").write(result)

View file

@ -1,111 +0,0 @@
#!/usr/bin/python3
import os
import json
import glob
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
###############################################################################
output_dir = "../www/"
template_path = os.path.join(output_dir,"template_appci_branch.html")
tests = [ "Package linter",
"Installation",
"Deleting",
"Upgrade",
"Backup",
"Restore",
"Change URL",
"Installation in a sub path",
"Deleting from a sub path",
"Installation on the root",
"Deleting from root",
"Installation in private mode",
"Installation in public mode",
"Multi-instance installations",
"Malformed path",
"Port already used" ]
ci_branches = [ ("stable", "Stable (x86)"),
("arm", "Stable (ARM)"),
("testing", "Testing"),
("unstable", "Unstable") ]
###############################################################################
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme)))
def shell_to_html(shell):
return conv.convert(shell, False)
###############################################################################
if __name__ == '__main__':
# Load the template
template = open(template_path, "r").read()
t = Template(template)
apps = [ file_.replace("data/", "") for file_ in glob.glob("data/*") ]
branch = ci_branches[0]
branch_id = branch[0]
data = { "ci_branch": branch,
"tests": tests,
"result_to_class": { None: "unknown",
False: "danger",
True: "success" }
}
data["apps"] = []
for app in apps:
data["apps"].append((app, json.loads(open("data/" + app).read())[branch_id]))
# Sort apps according to level, number of successfull test, name
def level(app):
test_results = app[1]
if not test_results:
return -1
if "level" in test_results and test_results["level"]:
return test_results["level"]
return 0
def test_score(app):
test_results = app[1]
if not test_results or "tests" not in test_results:
return -1
score = 0
for test, r in test_results["tests"].items():
if r == True:
score += 1
elif r == False:
score -= 1
return score
data["apps"] = sorted(data["apps"], key=lambda a: (-level(a), -test_score(a)))
#, lambda app,T: (T["level"],
# len([ t for t in T["tests"].values if t == True ]),
# app))
summary_per_level = []
summary_per_level.append(("Untested", len([ a for a in data["apps"] if level(a) == -1 ])))
for l in range(0, 8):
summary_per_level.append(("Level %s" % l, len([ a for a in data["apps"] if level(a) == l ])))
data["summary_per_level"] = summary_per_level
# Generate the output using the template
result = t.render(data=data, convert=shell_to_html, shell_css=shell_css)
output_path = os.path.join(output_dir, "appci_%s.html" % branch_id)
open(output_path, "w").write(result)
print "Done."

View file

@ -0,0 +1,69 @@
#!/usr/bin/python3
import os
import json
import glob
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
from common import tests, ci_branches
###############################################################################
output_dir = "../www/"
template_path = "./templates/branch_compare.html"
###############################################################################
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme)))
def shell_to_html(shell):
return conv.convert(shell, False)
###############################################################################
def main():
# Load the template
template = open(template_path, "r").read()
t = Template(template)
apps = [ file_.replace("data/", "") for file_ in glob.glob("data/*") ]
data = { "ci_branches": ci_branches,
}
# Sort apps according to level, number of successfull test, name
def level(test_results):
if not test_results:
return -1
if "level" in test_results and test_results["level"]:
return test_results["level"]
return 0
def compare_levels(a, b):
if a > b:
return '+'
if a < b:
return '-'
else:
return '='
data["apps"] = []
for app in apps:
all_test_results = json.loads(open("data/" + app).read())
all_levels = [ level(all_test_results[ci_branch]) for ci_branch, _ in ci_branches ]
data["apps"].append((app, all_levels))
data["apps"] = sorted(data["apps"], key=lambda a: -a[1][0])
# Generate the output using the template
result = t.render(data=data, convert=shell_to_html, shell_css=shell_css)
output_path = os.path.join(output_dir, "appci_branch_compare.html")
open(output_path, "w").write(result)

87
appci/publish_branches.py Executable file
View file

@ -0,0 +1,87 @@
#!/usr/bin/python3
import os
import json
import glob
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
from common import tests, ci_branches
###############################################################################
output_dir = "../www/"
template_path = "./templates/branch.html"
###############################################################################
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme)))
def shell_to_html(shell):
return conv.convert(shell, False)
###############################################################################
def main():
# Load the template
template = open(template_path, "r").read()
t = Template(template)
apps = [ file_.replace("data/", "") for file_ in glob.glob("data/*") ]
for branch in ci_branches:
branch_id = branch[0]
data = { "ci_branch": branch,
"tests": tests,
"result_to_class": { None: "unknown",
False: "danger",
True: "success" }
}
data["apps"] = []
for app in apps:
data["apps"].append((app, json.loads(open("data/" + app).read())[branch_id]))
# Sort apps according to level, number of successfull test, name
def level(app):
test_results = app[1]
if not test_results:
return -2
if "level" in test_results:
l = test_results["level"]
return l if not l is None else -1
return -1
def test_score(app):
test_results = app[1]
if not test_results or "tests" not in test_results:
return -1
score = 0
for test, r in test_results["tests"].items():
if r == True:
score += 1
elif r == False:
score -= 1
return score
data["apps"] = sorted(data["apps"], key=lambda a: (-level(a), -test_score(a)))
summary_per_level = []
summary_per_level.append(("Untested", len([ a for a in data["apps"] if level(a) == -1 ])))
for l in range(0, 8):
summary_per_level.append(("Level %s" % l, len([ a for a in data["apps"] if level(a) == l ])))
data["summary_per_level"] = summary_per_level
# Generate the output using the template
result = t.render(data=data, convert=shell_to_html, shell_css=shell_css)
output_path = os.path.join(output_dir, "appci", "branch", "%s.html" % branch_id)
open(output_path, "w").write(result)

View file

@ -6,8 +6,8 @@
<title>Apps CI Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="../css/bootstrap.css" media="screen">
<link rel="stylesheet" href="../skins/eden.css" media="screen">
<link rel="stylesheet" href="../../css/bootstrap.css" media="screen">
<link rel="stylesheet" href="../../skins/eden.css" media="screen">
<style>
#app-test-results
@ -67,8 +67,6 @@
<body>
<div class="bs-docs-section">
<div id="levelSummary" style="height: 270px; width: 100%;"></div>
<div class="row text-center">
<h2>{{ data.appname }}</h2>
</div>
@ -90,7 +88,7 @@
<tr id="ci-branch-{{ ci_branch_id }}">
<td class="ci-branch-name"><div title="CI Branch"><strong>{{ ci_branch_name }}</strong></div></td>
{% if data.results[ci_branch_id] %}
<td><div title="Level"><strong>{{ data.results[ci_branch_id]["level"] or '?' }}</strong></div></td>
<td><div title="Level"><strong>{{ data.results[ci_branch_id]["level"] }}</strong></div></td>
{% for test in data.tests %}
<td class="ci-test-result">
<div title="{{ test }}" class="{{ data.result_to_class[data.results[ci_branch_id]["tests"][test]] }}"></div>
@ -111,39 +109,8 @@
</div>
</div>
<script src="../js/jquery-2.1.3.min.js"></script>
<script src="../js/bootstrap.min.js"></script>
<script src="../js/canvasjs.min.js"></script>
<script>
window.onload = function () {
var chart = new CanvasJS.Chart("levelSummary", {
animationEnabled: true,
data: [{
type: "doughnut",
startAngle: -90,
//innerRadius: 60,
indexLabelFontSize: 17,
indexLabel: "{label} - #percent%",
toolTipContent: "<b>{label}:</b> {y} (#percent%)",
dataPoints: [
{ y: 100, label: "Untested", color: "#aaaaaa" },
{ y: 200, label: "Level 0", color: "#d9534f" },
{ y: 200, label: "Level 1", color: "#E26D4F" },
{ y: 100, label: "Level 2", color: "#E98D4E" },
{ y: 100, label: "Level 3", color: "#f0ad4e" },
{ y: 40, label: "Level 4", color: "#CBB052" },
{ y: 60, label: "Level 5", color: "#A6B255" },
{ y: 50, label: "Level 6", color: "#7AB659" },
{ y: 150, label: "Level 7", color: "#5cb85c" }
]
}]
});
chart.render();
}
</script>
<script src="../../js/jquery-2.1.3.min.js"></script>
<script src="../../js/bootstrap.min.js"></script>
</body>
</html>

View file

@ -6,8 +6,8 @@
<title>Apps CI Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="css/bootstrap.css" media="screen">
<link rel="stylesheet" href="skins/eden.css" media="screen">
<link rel="stylesheet" href="../../css/bootstrap.css" media="screen">
<link rel="stylesheet" href="../../skins/eden.css" media="screen">
<style>
#app-test-results
@ -69,7 +69,7 @@
<div class="row text-center">
<h2>{{ data.ci_branch[1] }}</h2>
<div id="levelSummary" style="height: 270px; width: 100%;"></div>
<div id="levelSummary" style="height: 270px;" class="col-sm-6 col-sm-offset-3"></div>
</div>
<div class="row">
@ -88,9 +88,9 @@
<tbody>
{% for app, test_results in data.apps %}
<tr id="ci-app-{{ app }}">
<td class="ci-app-name"><div title="App"><strong>{{ app }}</strong></div></td>
<td class="ci-app-name"><a href="../app/{{ app }}.html"><div title="App"><strong>{{ app }}</strong></div></a></td>
{% if test_results %}
<td><div title="Level"><strong>{{ test_results["level"] or '?' }}</strong></div></td>
<td><div title="Level"><strong>{{ '?' if test_results["level"] == None else test_results["level"] }}</strong></div></td>
{% for test in data.tests %}
<td class="ci-test-result">
<div title="{{ test }}" class="{{ data.result_to_class[test_results["tests"][test]] }}"></div>
@ -110,15 +110,15 @@
</div>
</div>
<script src="js/jquery-2.1.3.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/canvasjs.min.js"></script>
<script src="../../js/jquery-2.1.3.min.js"></script>
<script src="../../js/bootstrap.min.js"></script>
<script src="../../js/canvasjs.min.js"></script>
<script>
window.onload = function () {
var chart = new CanvasJS.Chart("levelSummary", {
animationEnabled: true,
animationEnabled: false,
data: [{
type: "doughnut",
startAngle: -90,
@ -127,15 +127,15 @@ var chart = new CanvasJS.Chart("levelSummary", {
indexLabel: "{label} - #percent%",
toolTipContent: "<b>{label}:</b> {y} (#percent%)",
dataPoints: [
{ y: {{ data.summary_per_level[0] }}, label: "Untested", color: "#cccccc" },
{ y: {{ data.summary_per_level[1] }}, label: "Level 0", color: "#d9534f" },
{ y: {{ data.summary_per_level[2] }}, label: "Level 1", color: "#E26D4F" },
{ y: {{ data.summary_per_level[3] }}, label: "Level 2", color: "#E98D4E" },
{ y: {{ data.summary_per_level[4] }}, label: "Level 3", color: "#f0ad4e" },
{ y: {{ data.summary_per_level[5] }}, label: "Level 4", color: "#CBB052" },
{ y: {{ data.summary_per_level[6] }}, label: "Level 5", color: "#A6B255" },
{ y: {{ data.summary_per_level[7] }}, label: "Level 6", color: "#7AB659" },
{ y: {{ data.summary_per_level[8] }}, label: "Level 7", color: "#5cb85c" }
{ y: {{ data.summary_per_level[0] }}, label: "Unknown", color: "#cccccc" },
{ y: {{ data.summary_per_level[1] }}, label: "Level 0", color: "#d9534f" },
{ y: {{ data.summary_per_level[2] }}, label: "Level 1", color: "#E26D4F" },
{ y: {{ data.summary_per_level[3] }}, label: "Level 2", color: "#E98D4E" },
{ y: {{ data.summary_per_level[4] }}, label: "Level 3", color: "#f0ad4e" },
{ y: {{ data.summary_per_level[5] }}, label: "Level 4", color: "#CBB052" },
{ y: {{ data.summary_per_level[6] }}, label: "Level 5", color: "#A6B255" },
{ y: {{ data.summary_per_level[7] }}, label: "Level 6", color: "#7AB659" },
{ y: {{ data.summary_per_level[8] }}, label: "Level 7", color: "#5cb85c" }
]
}]
});

View file

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Apps CI Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="css/bootstrap.css" media="screen">
<link rel="stylesheet" href="skins/eden.css" media="screen">
<style>
#app-test-results
{
margin-left:auto;
margin-right:auto;
/*width:1000px;*/
}
.navbar-holder-dark {
padding: 20px 20px 200px 20px;
background: #333333;
}
table, thead, tbody { display: block; width: 100%;}
table.apps { margin: 0 auto; margin-top: 100px; width: 350px; overflow-x: visible; }
th.ci-test-title
{
margin:5px;
white-space: nowrap;
}
th.ci-test-title > div
{
position:relative;
transform:
translate(0px, -10px)
rotate(315deg);
width: 33px;
}
th.ci-test-title > div > span
{
border-bottom: 1px solid #aaa;
padding: 0;
border:none;
}
/*tr { display: block; width: 100%; }*/
th, td { display: block; border: none; padding; 0px;float: left; height:33px; width: 33px; margin: 5px; }
th.ci-app-name, td.ci-app-name { text-align: center; width:130px; padding-top:9px !important; }
td.ci-test-result > div { position:relative; background-color: #bdc3c7; border-radius:5px; width: 100%; height:100%;}
td.ci-test-result { text-align:center; }
td.ci-test-result > div.success { background-color: rgb(46,204,113); }
td.ci-test-result > div.danger { background-color: rgb(225,80,62); }
.table > thead > tr > th { border : none; }
.table > tbody > tr > td { border : none; }
.canvasjs-chart-credit { display: none; }
</style>
</head>
<body>
<div class="bs-docs-section">
<div class="row text-center">
<h2>Branch comparison</h2>
</div>
<div class="row">
<div id="app-test-results">
<div>
<table class="table table-responsive apps">
<thead>
<tr>
<th class="ci-app-name"><div></div></th>
{% for branch_id, branch_name in data.ci_branches %}
<th class="ci-test-title"><div>{{ branch_name }}</div></th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for app, test_results in data.apps %}
<tr id="ci-app-{{ app }}">
<td class="ci-app-name"><div title="App"><strong>{{ app }}</strong></div></td>
{% for branch_level in test_results %}
<td><div title="Level"><strong>{{ branch_level }}</strong></div></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="js/jquery-2.1.3.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/canvasjs.min.js"></script>
</body>
</html>

0
www/appci/app/.gitkeep Normal file
View file

View file

View file

@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Apps CI Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="css/bootstrap.css" media="screen">
<link rel="stylesheet" href="skins/eden.css" media="screen">
<!--<link
href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet">-->
<link href="css/animate.css" rel="stylesheet">
<style>
#appci
{
margin-left:auto;
margin-right:auto;
/*width:1000px;*/
}
.navbar-holder-dark{
padding: 20px 20px 200px 20px;
background: #333333;
}
table, thead, tbody { display: block; width: 100%;}
table.apps { margin: 0 auto; margin-top: 200px; width: 1000px; overflow-x: visible; }
th.testname
{
margin:5px;
white-space: nowrap;
}
th.testname > div
{
position:relative;
transform:
translate(0px, -10px)
rotate(315deg);
width: 33px;
}
th.testname > div > span
{
border-bottom: 1px solid #aaa;
padding: 0;
border:none;
}
/*tr { display: block; width: 100%; }*/
th, td { display: block; border: none; padding; 0px;float: left; height:33px; width: 33px; margin: 5px; }
th.appname, td.appname { text-align: center; width:130px; padding-top:9px !important; }
td.appstatus > div { position:relative; background-color: #bdc3c7; border-radius:5px; width: 100%; height:100%;}
td.appstatus { text-align:center; }
td.appstatus > div.success { background-color: rgb(46,204,113); }
td.appstatus > div.danger { background-color: rgb(225,80,62); }
.table > thead > tr > th { border : none; }
.table > tbody > tr > td { border : none; }
</style>
</head>
<body>
<!-- Tables
================================================== -->
<div class="bs-docs-section">
<div class="row">
<div id="appci">
<div>
<table class="table table-responsive apps">
<thead>
<tr>
<th class="appname"><div></div></th>
<th class="testname"><div>Level</div></th>
{% for testname in data.testnames %}
<th class="testname"><div><span>{{ testname }}</span></div></th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for app in data.apps %}
<tr id="{{ app.name }}">
<td class="appname"><div title="App name"><strong>{{ app.name }}</strong></div></td>
<td class="applevel"><div title="Level"><strong>{{ app.level }}</strong></div></td>
{% for status in app.statuses %}
<td class="appstatus"><div title="{{ data.testnames[loop.index-1] }}" class="{{ status }}"></div></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script src="js/bootstrap.min.js"></script>
</body>
</html>