Initial commit
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*un~
|
||||||
|
*.swp
|
||||||
|
*.pyc
|
||||||
|
*.wsgi
|
3
config.local.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = "postgresql://tetawebapp:tetawebapp@localhost/tetawebapp"
|
||||||
|
UPLOADED_FILES_DEST = "./upload"
|
BIN
static/fonts/RobotoCondensed-Bold.ttf
Normal file
BIN
static/fonts/RobotoCondensed-Regular.ttf
Normal file
BIN
static/images/404.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
static/images/add.png
Normal file
After Width: | Height: | Size: 386 B |
BIN
static/images/dummy_pic.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
static/images/edit.png
Normal file
After Width: | Height: | Size: 938 B |
BIN
static/images/favicon.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/images/login.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
static/images/logo.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
static/images/logout.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
static/images/refresh.png
Normal file
After Width: | Height: | Size: 312 B |
BIN
static/images/save.png
Normal file
After Width: | Height: | Size: 357 B |
BIN
static/images/search.png
Normal file
After Width: | Height: | Size: 371 B |
BIN
static/images/trash.png
Normal file
After Width: | Height: | Size: 878 B |
BIN
static/images/upload.png
Normal file
After Width: | Height: | Size: 244 B |
249
static/scripts/tetawebapp.js
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
var red = "#FF0000";
|
||||||
|
var green = "#00FF00";
|
||||||
|
var light_red = "#FCD5DC";
|
||||||
|
var light_green = "#D5FCD8";
|
||||||
|
var base_bg = "#FFFFFF";
|
||||||
|
var base_border = "#888888";
|
||||||
|
var coloured_bg = "#FF5D00";
|
||||||
|
var clear_bg = "#E5E5E5";
|
||||||
|
var text_color = "#555555";
|
||||||
|
|
||||||
|
/* **************************************************************************************
|
||||||
|
* GLOBAL
|
||||||
|
* **************************************************************************************/
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
function setcookie(cname, cvalue, exdays) {
|
||||||
|
// Set cookie
|
||||||
|
var d = new Date();
|
||||||
|
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||||
|
var expires = "expires="+ d.toUTCString();
|
||||||
|
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getcookie(cname) {
|
||||||
|
// Get cookie by name
|
||||||
|
var value = "; " + document.cookie;
|
||||||
|
var parts = value.split("; " + cname + "=");
|
||||||
|
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eye candies
|
||||||
|
function valid_input(obj) {
|
||||||
|
// Valid input makes obj background to glow green for 2 seconds
|
||||||
|
// If obj borders were red, they get they normal color back
|
||||||
|
obj.style.backgroundColor = light_green;
|
||||||
|
obj.style.borderColor = base_border;
|
||||||
|
setTimeout( function() {
|
||||||
|
obj.style.backgroundColor = base_bg;
|
||||||
|
}
|
||||||
|
, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalid_input(obj) {
|
||||||
|
// Invalid input makes obj borders and background to glow red for 2 seconds
|
||||||
|
// Border color will stay red until a valid input is sent
|
||||||
|
obj.style.backgroundColor = light_red;
|
||||||
|
obj.style.borderColor = red;
|
||||||
|
setTimeout( function() {
|
||||||
|
obj.style.backgroundColor = base_bg;
|
||||||
|
}
|
||||||
|
, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function valid_upload(obj) {
|
||||||
|
// Valid input makes obj background to glow green for 2 seconds
|
||||||
|
// If obj borders were red, they get they normal color back
|
||||||
|
obj.style.backgroundColor = green;
|
||||||
|
obj.style.borderColor = text_color;
|
||||||
|
obj.style.borderStyle = 'solid';
|
||||||
|
setTimeout( function() {
|
||||||
|
obj.style.backgroundColor = clear_bg;
|
||||||
|
obj.style.borderStyle = 'none';
|
||||||
|
}
|
||||||
|
, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalid_upload(obj) {
|
||||||
|
// Invalid input makes obj borders and background to glow red for 2 seconds
|
||||||
|
// Border color will stay red until a valid input is sent
|
||||||
|
obj.style.backgroundColor = red;
|
||||||
|
obj.style.borderColor = text_color;
|
||||||
|
obj.style.borderStyle = 'solid';
|
||||||
|
setTimeout( function() {
|
||||||
|
obj.style.borderStyle = 'solid';
|
||||||
|
obj.style.backgroundColor = clear_bg;
|
||||||
|
obj.style.borderColor = red;
|
||||||
|
}
|
||||||
|
, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lit(obj) {
|
||||||
|
// Lit bacground and border on obj (use by input type=file)
|
||||||
|
obj.style.backgroundColor = coloured_bg;
|
||||||
|
obj.style.borderColor = text_color;
|
||||||
|
obj.style.borderStyle = 'solid';
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlit(obj) {
|
||||||
|
// Unlit bacground and border on obj (use by input type=file)
|
||||||
|
obj.style.backgroundColor = clear_bg;
|
||||||
|
obj.style.borderColor = clear_bg;
|
||||||
|
obj.style.borderStyle = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function verify_login() {
|
||||||
|
// Verify login inputs
|
||||||
|
login = document.getElementById('login');
|
||||||
|
password = document.getElementById('password');
|
||||||
|
if (login.value.length > 0) {
|
||||||
|
valid_input(login);
|
||||||
|
if (password.value.length > 0) {
|
||||||
|
valid_input(password);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
invalid_input(password);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
invalid_input(login);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
// Logout user
|
||||||
|
setcookie('token', '', 30);
|
||||||
|
document.location = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* **************************************************************************************
|
||||||
|
* AJAX
|
||||||
|
* **************************************************************************************/
|
||||||
|
|
||||||
|
function get_html_from_ajax(obj, url) {
|
||||||
|
// Get HTML content from AJAX request from url argument
|
||||||
|
// HTML content is then put as innerHTML to obj
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.onerror = function(){
|
||||||
|
obj.innerHTML = "Error while getting content (1)";
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onload = function(){
|
||||||
|
if (xhttp.status != 200) {
|
||||||
|
obj.innerHTML = "Error while getting content (2)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||||
|
var response = xhttp.responseText;
|
||||||
|
obj.innerHTML = response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhttp.open('POST', url, true);
|
||||||
|
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_value_from_ajax(obj, url, err_code) {
|
||||||
|
// Send value from obj.value via AJAX request to url argument
|
||||||
|
// obj.value is passed to URL in a REST sheme like <URL>/<VALUE>
|
||||||
|
// If err_code response is received, then a server side
|
||||||
|
// error has occured and input is invalidated.
|
||||||
|
url = url + '/' + obj.value;
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.onerror = function(){
|
||||||
|
invalid_input(obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onload = function(){
|
||||||
|
if (xhttp.status != 200) {
|
||||||
|
invalid_input(obj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||||
|
var response = xhttp.responseText;
|
||||||
|
if (response == err_code) {
|
||||||
|
invalid_input(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
valid_input(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhttp.open('POST', url, true);
|
||||||
|
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_value_from_ajax(obj, url, err_code) {
|
||||||
|
// Get value from AJAX request
|
||||||
|
// The returned value is then set to obj.value.
|
||||||
|
// If err_code response is received, then a server side
|
||||||
|
// error has occured and input is invalidated
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.onerror = function(){
|
||||||
|
invalid_input(obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onload = function(){
|
||||||
|
if (xhttp.status != 200) {
|
||||||
|
invalid_input(obj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||||
|
var response = xhttp.responseText;
|
||||||
|
if (response == err_code) {
|
||||||
|
invalid_input(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj.value = response;
|
||||||
|
valid_input(obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhttp.open('POST', url, true);
|
||||||
|
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function upload_file_from_ajax(obj, url, err_code) {
|
||||||
|
// Upload files get from <obj> to the specified <url>
|
||||||
|
// if <errcode> is returned input is invalidated
|
||||||
|
var files = obj.files;
|
||||||
|
var icon_id = obj.id.substring(obj.id.lastIndexOf("_") + 1);
|
||||||
|
var icon_obj = document.getElementById("upload_icon_" + icon_id)
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.onerror = function(){
|
||||||
|
invalid_upload(icon_obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onload = function(){
|
||||||
|
if (xhttp.status != 200) {
|
||||||
|
invalid_upload(icon_obj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||||
|
var response = xhttp.responseText;
|
||||||
|
if (response == err_code) {
|
||||||
|
invalid_upload(icon_obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
valid_upload(icon_obj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.open('POST', url, true);
|
||||||
|
var formData = new FormData();
|
||||||
|
for (var i=0; i < files.length; i++){
|
||||||
|
formData.append("files", files[i], files[i].name);
|
||||||
|
}
|
||||||
|
xhttp.send(formData);
|
||||||
|
}
|
29
static/styles/colors.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Here are the base color scheme and icon set.
|
||||||
|
* You can modify it or create your own using the same variables
|
||||||
|
* and make it loaded after this one but before the fonts.css in
|
||||||
|
* the HTML header section of the index.html template file.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--coloured-bg: #FF5D00;
|
||||||
|
--light-coloured-bg: #FFB387;
|
||||||
|
--clear-bg: #E5E5E5;
|
||||||
|
--mid-bg: #BBBBBB;
|
||||||
|
--dark-bg: #2B2B2B;
|
||||||
|
--dark-border: #888888;
|
||||||
|
--text-color: #555555;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--black: #000000;
|
||||||
|
--font-normal: url("/static/fonts/RobotoCondensed-Regular.ttf") format("truetype");
|
||||||
|
--font-bold: url("/static/fonts/RobotoCondensed-Bold.ttf") format("truetype");
|
||||||
|
--banner-logo: url(/static/images/logo.png);
|
||||||
|
--add_icon: url(/static/images/add.png);
|
||||||
|
--edit_icon: url(/static/images/edit.png);
|
||||||
|
--login_icon: url(/static/images/login.png);
|
||||||
|
--logout_icon: url(/static/images/logout.png);
|
||||||
|
--refresh_icon: url(/static/images/refresh.png);
|
||||||
|
--save_icon: url(/static/images/save.png);
|
||||||
|
--search_icon: url(/static/images/search.png);
|
||||||
|
--trash_icon: url(/static/images/trash.png);
|
||||||
|
--upload_icon: url(/static/images/upload.png);
|
||||||
|
}
|
20
static/styles/fonts.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Here are the font definitions.
|
||||||
|
* You can modify it or create your own and make it loaded
|
||||||
|
* after this one in the HTML header section of the index.html
|
||||||
|
* template file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: var(--font-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: var(--font-bold);
|
||||||
|
}
|
370
static/styles/tetawebapp.css
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
/*
|
||||||
|
* Do NOT modify this file:
|
||||||
|
* ------------------------
|
||||||
|
* If you want to add or modify classes, create a new
|
||||||
|
* CSS files and make it loaded after this one in the
|
||||||
|
* HTML header section of the index.html template file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 10px;
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
background-color: var(--dark-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
display: flex;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article {
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--clear-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > section.inline {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--clear-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > section.inline > article.left {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > section.inline > article.right {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content > nav.vertical {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
background-color: var(--clear-bg);
|
||||||
|
border-right-color: var(--mid-bg);
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content > nav.vertical {
|
||||||
|
order: -1;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content > nav.vertical > a {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--clear-bg);
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content > nav.vertical > a:hover {
|
||||||
|
background-color: var(--coloured-bg);
|
||||||
|
color: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content > nav.vertical > a.selected {
|
||||||
|
background-color: var(--light-coloured-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--clear-bg);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal {
|
||||||
|
display: inline-block;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-color: var(--coloured-bg);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal > li {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal > li > a {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-color);
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal > li > a:hover {
|
||||||
|
background-color: var(--coloured-bg);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal > li > a.right_border {
|
||||||
|
border-right-color: var(--coloured-bg);
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div.navbar_container > ul.horizontal > li > a.selected {
|
||||||
|
background-color: var(--light-coloured-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article {
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--text-color);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article.error,
|
||||||
|
main > article.error > p {
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--text-color);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > h3,
|
||||||
|
main > section.inline > article > h3 {
|
||||||
|
font-size: 30px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > p,
|
||||||
|
main > article > ul,
|
||||||
|
main > article > ol {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-align: justify;
|
||||||
|
text-justify: distribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > hr {
|
||||||
|
border-color: var(--mid-bg);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > img {
|
||||||
|
display:inline-block;
|
||||||
|
border-color: var(--mid-bg);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > p > a {
|
||||||
|
color: var(--coloured-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > p > a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article.right > img {
|
||||||
|
float: right;
|
||||||
|
margin: 0 0 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article.left > img {
|
||||||
|
float: left;
|
||||||
|
margin: 0 10px 0px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
height: 65px;
|
||||||
|
font-size: 34px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--white);
|
||||||
|
background: var(--banner-logo);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 10px;
|
||||||
|
text-shadow: 0 0 1px var(--black);
|
||||||
|
border-bottom-color: var(--dark-border);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-top-color: var(--white);
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
height: 35px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em;
|
||||||
|
border-bottom-color: var(--white);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-top-color: var(--dark-border);
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
background-color: var(--coloured-bg);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
pre {
|
||||||
|
border-color: var(--dark-border);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 5px;
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
border-color: var(--coloured-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"] {
|
||||||
|
border-color: var(--dark-border);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
background-color: var(--coloured-bg);
|
||||||
|
color: var(--white);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px;
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
input[type="button"]:hover,
|
||||||
|
input[type="submit"]:hover,
|
||||||
|
input[type="file"]:hover {
|
||||||
|
background-color: var(--light-coloured-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.file_upload {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--clear-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input.add,
|
||||||
|
input.edit,
|
||||||
|
input.login,
|
||||||
|
input.logout,
|
||||||
|
input.refresh,
|
||||||
|
input.save,
|
||||||
|
input.search,
|
||||||
|
input.trash,
|
||||||
|
input.upload {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.add:hover,
|
||||||
|
input.edit:hover,
|
||||||
|
input.login:hover,
|
||||||
|
input.logout:hover,
|
||||||
|
input.refresh:hover,
|
||||||
|
input.save:hover,
|
||||||
|
input.search:hover,
|
||||||
|
input.trash:hover,
|
||||||
|
input.upload:hover {
|
||||||
|
border-color: var(--text-color);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
background-color: var(--coloured-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.add {
|
||||||
|
background: var(--add_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.edit {
|
||||||
|
background: var(--edit_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.login {
|
||||||
|
background: var(--login_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.logout {
|
||||||
|
background: var(--logout_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.refresh {
|
||||||
|
background: var(--refresh_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.save {
|
||||||
|
background: var(--save_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.search {
|
||||||
|
background: var(--search_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.trash {
|
||||||
|
background: var(--trash_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
input.upload {
|
||||||
|
background: var(--upload_icon);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
49
templates/ajax.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Ajax{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<section class='inline'>
|
||||||
|
<article class='left'>
|
||||||
|
<h3>Get HTML response from AJAX</h3>
|
||||||
|
<p>Click the refresh button to get the HTML response.</p>
|
||||||
|
<p>The response may randomly be a voluntary error so you should try it more than once.</p>
|
||||||
|
Refresh: <input type='button' class='refresh' value=' '
|
||||||
|
onclick='javascript:get_html_from_ajax(document.getElementById("html_container"), "/get_html_from_ajax");'>
|
||||||
|
</article>
|
||||||
|
<article class='right'>
|
||||||
|
<h3>Upload files with AJAX</h3>
|
||||||
|
<p>Select files to upload</p>
|
||||||
|
<p>The response may randomly be a voluntary error so you should try it more than once.</p>
|
||||||
|
Upload files:
|
||||||
|
<div class='file_upload'>
|
||||||
|
<!--
|
||||||
|
Input file is a tricky hack (see tetawebapp.css and tetawebapp.js)
|
||||||
|
-->
|
||||||
|
<input type='button' id='upload_icon_1' class='upload' title='Upload' value=' '/>
|
||||||
|
<input type='file' id='upload_input_1' multiple
|
||||||
|
title='Upload'
|
||||||
|
onchange='javascript:upload_file_from_ajax(this, "/upload", "TETA_ERR");'
|
||||||
|
onmouseover='javascript:lit(document.getElementById("upload_icon_1"));'
|
||||||
|
onmouseout='javascript:unlit(document.getElementById("upload_icon_1"));'/>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<hr/>
|
||||||
|
<article class='right' id='html_container'></article>
|
||||||
|
<hr/>
|
||||||
|
<section class='inline'>
|
||||||
|
<article class='left'>
|
||||||
|
<h3>Set value via AJAX</h3>
|
||||||
|
<p>Send value to the application.</p>
|
||||||
|
<p>If value is empty or is "We Make Porn" (case sensitive), an error is raised.</p>
|
||||||
|
<input type='text' id='value_sender'>
|
||||||
|
<input type='button' value="Try me" onclick='javascript:set_value_from_ajax(document.getElementById("value_sender"), "/set_value_from_ajax", "TETA_ERR");'>
|
||||||
|
</article>
|
||||||
|
<article class='right'>
|
||||||
|
<h3>Get value from AJAX</h3>
|
||||||
|
<p>Get a random value from the application.</p>
|
||||||
|
<p>Randomly raises a voluntary error so you should try it more than once.</p>
|
||||||
|
<input type='text' id='value_receiver'>
|
||||||
|
<input type='button' value="Try me" onclick='javascript:get_value_from_ajax(document.getElementById("value_receiver"), "/get_value_from_ajax", "TETA_ERR");'>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
24
templates/ajax_html.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<h3>This is the title</h3>
|
||||||
|
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||||
|
sed do eiusmod tempor incididunt ut labore
|
||||||
|
</p>
|
||||||
|
<p>This <a href='/plop.html'>link</a> will lead to an error page</p>
|
||||||
|
<p>
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia desers unt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
12
templates/articles.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Articles{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article>
|
||||||
|
<h3>Choose your article</h3>
|
||||||
|
<p>
|
||||||
|
Please select your article
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article id='article_receiver'>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
65
templates/articles_by_id.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Articles{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>Article #{{ ID }}</h3>
|
||||||
|
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||||
|
sed do eiusmod tempor incididunt ut labore
|
||||||
|
</p>
|
||||||
|
<p>This <a href='/plop.html'>link</a> will lead to an error page</p>
|
||||||
|
<p>
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia desers unt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>plop</li>
|
||||||
|
<li>plap</li>
|
||||||
|
<li>plip</li>
|
||||||
|
</ul>
|
||||||
|
<ol>
|
||||||
|
<li>plop</li>
|
||||||
|
<li>plap</li>
|
||||||
|
<li>plip</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class='left'>
|
||||||
|
<h3>Another disposition</h3>
|
||||||
|
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||||
|
sed do eiusmod tempor incididunt ut labore
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||||
|
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||||
|
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
||||||
|
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
|
||||||
|
officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
68
templates/basics.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Basics{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>Basics</h3>
|
||||||
|
<p>
|
||||||
|
Thanks to <a href='http://flask.pocoo.org/'>Python/Flask</a> with <strong>TetaWebApp</strong> most of the output things come to life via
|
||||||
|
<a href='http://jinja.pocoo.org/docs/2.10/'>Jinja2 HTML templates</a>
|
||||||
|
and is 100% <strong title='Bulshit inside'>HTML5 ready©</strong>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Colors and fonts are managed from separated CSS files letting you easily
|
||||||
|
change the default theme to your favorite colors and icon set.
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
/*
|
||||||
|
* Here are the font definitions.
|
||||||
|
* You can modify it or create your own and make it loaded
|
||||||
|
* after this one in the HTML header section of the index.html
|
||||||
|
* template file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: var(--font-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Condensed";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: var(--font-bold);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<pre>
|
||||||
|
/*
|
||||||
|
* Here are the base color scheme and icon set.
|
||||||
|
* You can modify it or create your own using the same variables
|
||||||
|
* and make it loaded after this one but before the fonts.css in
|
||||||
|
* the HTML header section of the index.html template file.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--coloured-bg: #FF5D00;
|
||||||
|
--light-coloured-bg: #FFB387;
|
||||||
|
--clear-bg: #E5E5E5;
|
||||||
|
--mid-bg: #BBBBBB;
|
||||||
|
--dark-bg: #2B2B2B;
|
||||||
|
--dark-border: #888888;
|
||||||
|
--text-color: #555555;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--black: #000000;
|
||||||
|
--font-normal: url("/static/fonts/RobotoCondensed-Regular.ttf") format("truetype");
|
||||||
|
--font-bold: url("/static/fonts/RobotoCondensed-Bold.ttf") format("truetype");
|
||||||
|
--banner-logo: url(/static/images/logo.png);
|
||||||
|
--add_icon: url(/static/images/add.png);
|
||||||
|
--edit_icon: url(/static/images/edit.png);
|
||||||
|
--login_icon: url(/static/images/login.png);
|
||||||
|
--logout_icon: url(/static/images/logout.png);
|
||||||
|
--refresh_icon: url(/static/images/refresh.png);
|
||||||
|
--save_icon: url(/static/images/save.png);
|
||||||
|
--search_icon: url(/static/images/search.png);
|
||||||
|
--trash_icon: url(/static/images/trash.png);
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
12
templates/database.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Database{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>Accessing database</h3>
|
||||||
|
<p>
|
||||||
|
Even if using <a href='http://flask-sqlalchemy.pocoo.org/2.3/'>Flask-SQLAlchemy</a> to retrieve data
|
||||||
|
stored in <strong>Postgres</strong> databases is the recommended way to use <strong>TetaWebApp</strong>,
|
||||||
|
you're free to use the database connector that suits your need.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
15
templates/error.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Erreur{% endblock %}
|
||||||
|
{% block nav %}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='error'>
|
||||||
|
<h3>404 - Not found</h3>
|
||||||
|
<p>The page you asked for was not found on this server.<br/>
|
||||||
|
It may has been lost, it may has never existed.<br/>
|
||||||
|
Maybe we don't care at all...</p>
|
||||||
|
<p>
|
||||||
|
<input type='button' value='Get me back to business' onclick='javascript:document.location="/";'/>
|
||||||
|
</p>
|
||||||
|
<img src='/static/images/404.png' alt='404 - Not found' title='404 - Not found'/>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
106
templates/index.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang='zxx'>
|
||||||
|
<head>
|
||||||
|
<title>TetaWebApp - {% block title %}Accueil{% endblock %}</title>
|
||||||
|
<meta name="viewport" content="initial-scale=1.0" />
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/styles/colors.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/styles/fonts.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/styles/tetawebapp.css" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/images/favicon.png" />
|
||||||
|
<script src="/static/scripts/tetawebapp.js"></script>
|
||||||
|
</head>
|
||||||
|
{% block bodyheader %}
|
||||||
|
<body>
|
||||||
|
{% endblock %}
|
||||||
|
<header>{% block banner %}TetaWebApp{% endblock %}</header>
|
||||||
|
<div class='content'>
|
||||||
|
{% block nav %}
|
||||||
|
<nav class='vertical'>
|
||||||
|
{% block menu %}
|
||||||
|
{% for item in menu %}
|
||||||
|
{% for key in item[1] %}
|
||||||
|
{% if item[2] == 1 %}
|
||||||
|
<a class='selected' href='{{ key }}'>{{ item[0] }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href='{{ key }}'>{{ item[0] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
</nav>
|
||||||
|
{% endblock%}
|
||||||
|
<main>
|
||||||
|
{% if navbar %}
|
||||||
|
<div class='navbar_container'>
|
||||||
|
<ul class='horizontal'>
|
||||||
|
{% for item in navbar %}
|
||||||
|
{% set selected = ['', 'selected'] %}
|
||||||
|
{% set last = ['right_border', 'last'] %}
|
||||||
|
{% for url in item[1] %}
|
||||||
|
<li><a class='{{ last[item[3]] }} {{ selected[item[2]] }}' href='{{ url }}'>{{ item[0] }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>TetaWebApp demo</h3>
|
||||||
|
<p>
|
||||||
|
Welcome to the <strong>TetaWebApp</strong> demo
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
TetaWebApp is a basic web application template based on <a href='http://flask.pocoo.org/'>Python/Flask</a>
|
||||||
|
and <a href='https://www.w3schools.com/js/js_ajax_intro.asp'>AJAX</a> made by
|
||||||
|
<a href='mailto:doug.letough@free.fr'>Doug Le Tough</a> from <a href='https://www.tetalab.org'>Tetalab</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The goal of this project is to provide a basic framework to make any web application you need while
|
||||||
|
letting you complete freedom on how to use or extend it <strong>without</strong> using any Google,
|
||||||
|
Bootstrap or any other piece of <strong>shitty free spyware</strong>.
|
||||||
|
</p>
|
||||||
|
TetaWebApp will <strong>never</strong> download or upload anything in any way.
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
<p>There is <strong>no</strong> limitation, you can use all or only parts of <strong>TetaWebApp</strong>
|
||||||
|
and you can <strong title='bullshit inside'>virtually</strong> do any app you want with TetaWebApp.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
But be sure that freedom has a cost: You <strong>will</strong> need work to make it work ;-)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>TetaWebApp</strong> is released under the only real <strong>free</strong> license: The
|
||||||
|
<a href='http://www.wtfpl.net/'><img src='http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-2.png'
|
||||||
|
title='WTFPL' alt='WTFPL' /></a>.
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
Get a copy of <strong>TetaWebApp</strong>:<br/>
|
||||||
|
<pre>
|
||||||
|
git clone git://git.tetalab.org/tetalab/tetawebapp
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% block footer %}
|
||||||
|
<footer>© - Tetalab - Le hacker space Toulousaing' putaing' cong' -</footer>
|
||||||
|
{% endblock%}
|
||||||
|
</body>
|
||||||
|
</html>
|
53
templates/inputs.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Inputs{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>The input collection</h3>
|
||||||
|
<p>
|
||||||
|
Have a look to the input collection:
|
||||||
|
</p>
|
||||||
|
<input type='text'/>
|
||||||
|
<button>Click me</button>
|
||||||
|
<br/>
|
||||||
|
<textarea cols='25'></textarea>
|
||||||
|
<br/>
|
||||||
|
<select>
|
||||||
|
{% for item in menu %}
|
||||||
|
<option>{{ item[0] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input type='submit' value='Click me too'/>
|
||||||
|
<br/>
|
||||||
|
<input type='button' value='And me !' />
|
||||||
|
<br/>
|
||||||
|
<input type='button' class='add' title='Add' value=' '/>
|
||||||
|
<input type='button' class='edit' title='Edit' value=' '/>
|
||||||
|
<input type='button' class='login' title='Login' value=' '/>
|
||||||
|
<input type='button' class='logout' title='Logout' value=' ' onclick='javascript:logout();'/>
|
||||||
|
<input type='button' class='refresh' title='Refresh' value=' '/>
|
||||||
|
<input type='button' class='save' title='Save' value=' '/>
|
||||||
|
<input type='button' class='search' title='Search' value=' '/>
|
||||||
|
<input type='button' class='trash' title='Trash' value=' '/>
|
||||||
|
<!--
|
||||||
|
Input file is a tricky hack (see tetawebapp.css and tetawebapp.js)
|
||||||
|
-->
|
||||||
|
<div class='file_upload'>
|
||||||
|
<input type='button' id='upload_icon_1' class='upload' title='Upload' value=' '/>
|
||||||
|
<input type='file' id='upload_input_1' name='files' multiple
|
||||||
|
title='Upload'
|
||||||
|
onchange='javascript:upload_file_from_ajax(this, "/upload", "TETA_ERR");'
|
||||||
|
onmouseover='javascript:lit(document.getElementById("upload_icon_1"));'
|
||||||
|
onmouseout='javascript:unlit(document.getElementById("upload_icon_1"));'/>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<pre>
|
||||||
|
#!/bin/sh
|
||||||
|
# This is code sample
|
||||||
|
while [ 1 ]
|
||||||
|
do
|
||||||
|
echo "Tits or GTFO !"
|
||||||
|
sleep .1
|
||||||
|
done
|
||||||
|
</pre>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
23
templates/login.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
{% block nav %}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='login'>
|
||||||
|
<h3>Login</h3>
|
||||||
|
<p>The demo login is:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Login: demo</li>
|
||||||
|
<li>Password: demo</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
{% if message != '' %}
|
||||||
|
<pre>{{ message }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
<article class='left'>
|
||||||
|
<form method='POST' action='/login'>
|
||||||
|
Login: <input id='login' name='login' type='text' />
|
||||||
|
Password: <input id='password' name='password' type='password' />
|
||||||
|
<input type='submit' value='Log me in' onclick='javascript:return verify_login();'>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
18
templates/todo.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}TODO{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<article class='right'>
|
||||||
|
<h3>TODO list</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strike>Basic menu management</strike></li>
|
||||||
|
<li>Installation wizard</li>
|
||||||
|
<li>Back office for basic content management</li>
|
||||||
|
<li><strike>Basic Ajax support</strike></li>
|
||||||
|
<li><strike>Session management</strike></li>
|
||||||
|
<li>File upload</li>
|
||||||
|
<li>Basic documentation</li>
|
||||||
|
<li><strike>Horizontal navbar</strike></li>
|
||||||
|
<li><strike>License</strike></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
304
tetawebapp.py
Executable file
@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Required modules
|
||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
import random
|
||||||
|
import binascii
|
||||||
|
import bcrypt
|
||||||
|
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
# Optionnal modules
|
||||||
|
import psycopg2
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# App settings
|
||||||
|
########################################################################
|
||||||
|
app = Flask(__name__)
|
||||||
|
# Path to static files
|
||||||
|
app.static_url_path='/static'
|
||||||
|
# Set debug mode to False for production
|
||||||
|
app.debug = True
|
||||||
|
# Various configuration settings belong here (optionnal)
|
||||||
|
app.config.from_pyfile('config.local.py')
|
||||||
|
# Generate a new key: head -n 40 /dev/urandom | md5sum | cut -d' ' -f1
|
||||||
|
app.secret_key = 'ce1d1c9ff0ff388a838b3a1e3207dd27'
|
||||||
|
# Feel free to use SQLAlchemy (or not)
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Sample user database
|
||||||
|
########################################################################
|
||||||
|
class Tetawebapp_users(db.Model):
|
||||||
|
__tablename__ = 'tetawebapp_users'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
mail = db.Column(db.Text, nullable=False)
|
||||||
|
password = db.Column(db.Text, nullable=False)
|
||||||
|
name = db.Column(db.Text, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Menu and navigation management
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
def get_menu(page):
|
||||||
|
""" The main menu is a list of lists in the followin format:
|
||||||
|
[unicode caption,
|
||||||
|
{unicode URL endpoint: [unicode route, ...]},
|
||||||
|
int 0]
|
||||||
|
- The URL end point is the URL where to point to (href)
|
||||||
|
- One of the routes MUST match the route called by request
|
||||||
|
- The int 0 is used to determine which menu entry is actally called.
|
||||||
|
The value MUST be 0."""
|
||||||
|
menu = [[u'Home', {u'/': [u'/']}, 0],
|
||||||
|
[u'Articles', {u'/articles': [u'/articles', u'/articles/<ID>']}, 0],
|
||||||
|
[u'Basics', {u'/basics': [u'/basics']}, 0],
|
||||||
|
[u'Inputs', {u'/inputs': [u'/inputs']}, 0],
|
||||||
|
[u'Ajax', {u'/ajax': [u'/ajax']}, 0],
|
||||||
|
[u'Database', {u'/database': [u'/database']}, 0],
|
||||||
|
[u'Todo', {u'/todo': [u'/todo']}, 0],
|
||||||
|
]
|
||||||
|
for item in menu:
|
||||||
|
for url in item[1]:
|
||||||
|
for route in item[1][url]:
|
||||||
|
if route == page:
|
||||||
|
item[2] = 1
|
||||||
|
return menu
|
||||||
|
# This should never happen
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def get_navbar(page, selected):
|
||||||
|
""" The horizontal navbar is a list of lists in the followin format:
|
||||||
|
[unicode caption, {unicode URL endpoint: [unicode route, ...]}, int 0]
|
||||||
|
- The URL end point is the URL where to point to (href)
|
||||||
|
- One of the routes MUST match the route called by request
|
||||||
|
- The int 0 is used to de """
|
||||||
|
navbars = [[u'First article', {u'/articles/1': [u'/articles', u'/articles/<ID>']}, 0, 0],
|
||||||
|
[u'Second article', {u'/articles/2': [u'/articles', u'/articles/<ID>']}, 0, 0],
|
||||||
|
[u'Third article', {u'/articles/3': [u'/articles', u'/articles/<ID>']}, 0, 0]
|
||||||
|
]
|
||||||
|
navbar = []
|
||||||
|
for item in navbars:
|
||||||
|
for url in item[1]:
|
||||||
|
if url == selected:
|
||||||
|
item[2] = 1
|
||||||
|
for route in item[1][url]:
|
||||||
|
if route == page:
|
||||||
|
navbar.append(item)
|
||||||
|
navbar[len(navbar) - 1][3] = 1
|
||||||
|
return navbar
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Session management
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
def sync_session(request, session):
|
||||||
|
""" Synchronize cookies with session """
|
||||||
|
for key in request.cookies:
|
||||||
|
session[key] = request.cookies[key].encode('utf8')
|
||||||
|
|
||||||
|
def sync_cookies(response, session):
|
||||||
|
""" Synchronize session with cookies """
|
||||||
|
for key in session:
|
||||||
|
response.set_cookie(key, value=str(session[key]))
|
||||||
|
|
||||||
|
def check_session(func):
|
||||||
|
""" Check if the session has required token cookie set.
|
||||||
|
If not, redirects to the login page. """
|
||||||
|
@wraps(func)
|
||||||
|
def check(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
if session['token'] == request.cookies['token'] and len(session['token']) > 0:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
session['token'] = ''
|
||||||
|
response = app.make_response(render_template('login.html', message=''))
|
||||||
|
sync_cookies(response, session)
|
||||||
|
return response
|
||||||
|
except KeyError:
|
||||||
|
return render_template('login.html', message='')
|
||||||
|
return check
|
||||||
|
|
||||||
|
def check_login(login, password):
|
||||||
|
""" Puts the login verification code here """
|
||||||
|
password = password.encode('utf-8')
|
||||||
|
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
|
stored_hash = Tetawebapp_users.query.filter_by(mail=login).with_entities(Tetawebapp_users.password).first()
|
||||||
|
if stored_hash:
|
||||||
|
if bcrypt.checkpw(password, stored_hash[0].encode('utf-8')):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def gen_token():
|
||||||
|
""" Generate a random token to be stored in session and cookie """
|
||||||
|
token = binascii.hexlify(os.urandom(42))
|
||||||
|
return token
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Routes:
|
||||||
|
# -------
|
||||||
|
# Except for the index function, the function name MUST have the same
|
||||||
|
# name than the URL endpoint to make the menu work properly
|
||||||
|
########################################################################
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
""" 404 not found """
|
||||||
|
return render_template('error.html'), 404
|
||||||
|
|
||||||
|
@app.route("/login", methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
login = request.form.get('login')
|
||||||
|
password = request.form.get('password')
|
||||||
|
if check_login(login, password):
|
||||||
|
# Generate and store a token in session
|
||||||
|
session['token'] = gen_token()
|
||||||
|
# Return user to index page
|
||||||
|
page = '/'
|
||||||
|
menu = get_menu(page)
|
||||||
|
response = app.make_response(render_template('index.html', menu=menu))
|
||||||
|
# Push token to cookie
|
||||||
|
sync_cookies(response, session)
|
||||||
|
return response
|
||||||
|
# Credentials are not valid
|
||||||
|
response = app.make_response(render_template('login.html', message='Invalid user or password'))
|
||||||
|
session['token'] = ''
|
||||||
|
sync_cookies(response, session)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route("/", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def index():
|
||||||
|
""" Index page """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('index.html', menu=menu)
|
||||||
|
|
||||||
|
@app.route("/articles", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def articles():
|
||||||
|
""" Arcticles page """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
navbar = get_navbar(page, '')
|
||||||
|
return render_template('articles.html', menu=menu, navbar=navbar)
|
||||||
|
|
||||||
|
@app.route("/articles/<ID>", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def articles_by_id(ID):
|
||||||
|
""" Arcticles page """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
selected = page.replace('<ID>', ID)
|
||||||
|
navbar = get_navbar(page, selected)
|
||||||
|
return render_template('articles_by_id.html', menu=menu, navbar=navbar, ID=ID)
|
||||||
|
|
||||||
|
@app.route("/basics", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def basics():
|
||||||
|
""" Basics page """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('basics.html', menu=menu)
|
||||||
|
|
||||||
|
@app.route("/inputs", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def inputs():
|
||||||
|
""" Show the input collection """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('inputs.html', menu=menu)
|
||||||
|
|
||||||
|
@app.route("/ajax", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def ajax():
|
||||||
|
""" Propose various AJAX tests """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('ajax.html', menu=menu)
|
||||||
|
|
||||||
|
@app.route("/database", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def database():
|
||||||
|
""" A blah on using databases """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('database.html', menu=menu)
|
||||||
|
|
||||||
|
@app.route("/todo", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def todo():
|
||||||
|
""" The famous TODO list """
|
||||||
|
page = str(request.url_rule)
|
||||||
|
menu = get_menu(page)
|
||||||
|
return render_template('todo.html', menu=menu)
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# AJAX routes
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
@app.route("/get_html_from_ajax", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def get_html_from_ajax():
|
||||||
|
""" Return HTML code to an AJAX request
|
||||||
|
It may generate a 404 http error for testing purpose """
|
||||||
|
if int(random.random()*10) % 2:
|
||||||
|
# Randomly generate 404 HTTP response
|
||||||
|
return render_template('error.html'), 404
|
||||||
|
return render_template('ajax_html.html')
|
||||||
|
|
||||||
|
@app.route("/get_value_from_ajax", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def get_value_from_ajax():
|
||||||
|
""" Return a randomly generated value to an AJAX request
|
||||||
|
It may return an error code for testing purpose """
|
||||||
|
err_code = 'TETA_ERR'
|
||||||
|
RND = int(random.random()*10)
|
||||||
|
if RND % 2:
|
||||||
|
# Randomly generate error
|
||||||
|
return err_code
|
||||||
|
return str(RND)
|
||||||
|
|
||||||
|
@app.route("/set_value_from_ajax/<value>", methods=['GET', 'POST'])
|
||||||
|
@check_session
|
||||||
|
def set_value_from_ajax(value):
|
||||||
|
""" Accept a value from an AJAX request
|
||||||
|
It may return an error code for testing purpose """
|
||||||
|
err_code = 'TETA_ERR'
|
||||||
|
if value != 'We Make Porn':
|
||||||
|
return 'True'
|
||||||
|
return err_code
|
||||||
|
|
||||||
|
@app.route("/upload", methods=['POST'])
|
||||||
|
@check_session
|
||||||
|
def upload():
|
||||||
|
""" Save a file from AJAX request
|
||||||
|
Files are saved in UPLOADED_FILES_DEST (see config.local.py) """
|
||||||
|
err_code = 'TETA_ERR'
|
||||||
|
RND = int(random.random()*10)
|
||||||
|
if RND % 2:
|
||||||
|
# Randomly generate error
|
||||||
|
print err_code
|
||||||
|
return err_code
|
||||||
|
uploaded_files = []
|
||||||
|
if len(request.files) > 0 and request.files['files']:
|
||||||
|
uploaded_files = request.files.getlist("files")
|
||||||
|
print "Uploaded files:"
|
||||||
|
for f in uploaded_files:
|
||||||
|
print ' [+] %s [%s]' % (f.filename, f.content_type)
|
||||||
|
# Before saving you should:
|
||||||
|
# - Secure the filename
|
||||||
|
# - Check file size
|
||||||
|
# - Check content type
|
||||||
|
f.save(os.path.join(app.config['UPLOADED_FILES_DEST'], f.filename))
|
||||||
|
f.close()
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Main
|
||||||
|
########################################################################
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0')
|
48
tetawebapp.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
\echo ******************************
|
||||||
|
\echo * Dropping database tetawebapp
|
||||||
|
\echo ******************************
|
||||||
|
|
||||||
|
\c postgres;
|
||||||
|
drop database tetawebapp;
|
||||||
|
|
||||||
|
\echo **************************
|
||||||
|
\echo * Dropping role tetawebapp
|
||||||
|
\echo **************************
|
||||||
|
drop role tetawebapp;
|
||||||
|
|
||||||
|
\echo ***************************************************
|
||||||
|
\echo * Creating role tetawebapp with password tetawebapp
|
||||||
|
\echo ***************************************************
|
||||||
|
create role tetawebapp with LOGIN ENCRYPTED PASSWORD 'tetawebapp';
|
||||||
|
|
||||||
|
\echo ******************************
|
||||||
|
\echo * Creating database tetawebapp
|
||||||
|
\echo ******************************
|
||||||
|
create database tetawebapp;
|
||||||
|
|
||||||
|
\echo *******************************************
|
||||||
|
\echo * Giving tetawebapp ownership to tetawebapp
|
||||||
|
\echo *******************************************
|
||||||
|
alter database tetawebapp owner to tetawebapp;
|
||||||
|
|
||||||
|
\echo *********************************
|
||||||
|
\echo * Creating tetawebapp_users table
|
||||||
|
\echo *********************************
|
||||||
|
|
||||||
|
\c tetawebapp;
|
||||||
|
CREATE TABLE tetawebapp_users (
|
||||||
|
id serial primary key,
|
||||||
|
mail text not NULL,
|
||||||
|
password text not NULL,
|
||||||
|
name text not NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo *************************************************
|
||||||
|
\echo * Giving tetawebapp_users ownership to tetawebapp
|
||||||
|
\echo *************************************************
|
||||||
|
alter table tetawebapp_users owner to tetawebapp;
|
||||||
|
|
||||||
|
\echo *********************************************************************
|
||||||
|
\echo * Inserting user demo identified by password demo to tetawebapp_users
|
||||||
|
\echo *********************************************************************
|
||||||
|
insert into tetawebapp_users (mail, password, name) values ('demo', '$2b$12$yjv4QMctGJFj2HmmbF6u5uDq9ATIl/Y9Z96MbaqRrcG6AE0CGHKSS', 'demo');
|