"Initial commit"
4
participer.thsf.net/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*un~
|
||||
*.swp
|
||||
*.pyc
|
||||
*.wsgi
|
3
participer.thsf.net/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"
|
1
participer.thsf.net/config.py
Symbolic link
@ -0,0 +1 @@
|
||||
config.local.py
|
BIN
participer.thsf.net/static/fonts/RobotoCondensed-Bold.ttf
Normal file
BIN
participer.thsf.net/static/fonts/RobotoCondensed-Regular.ttf
Normal file
BIN
participer.thsf.net/static/images/404.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
participer.thsf.net/static/images/add.png
Normal file
After Width: | Height: | Size: 386 B |
BIN
participer.thsf.net/static/images/dummy_pic.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
participer.thsf.net/static/images/edit.png
Normal file
After Width: | Height: | Size: 938 B |
BIN
participer.thsf.net/static/images/favicon.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
participer.thsf.net/static/images/login.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
participer.thsf.net/static/images/logo.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
participer.thsf.net/static/images/logout.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
participer.thsf.net/static/images/refresh.png
Normal file
After Width: | Height: | Size: 312 B |
BIN
participer.thsf.net/static/images/save.png
Normal file
After Width: | Height: | Size: 357 B |
BIN
participer.thsf.net/static/images/search.png
Normal file
After Width: | Height: | Size: 371 B |
BIN
participer.thsf.net/static/images/trash.png
Normal file
After Width: | Height: | Size: 878 B |
BIN
participer.thsf.net/static/images/upload.png
Normal file
After Width: | Height: | Size: 244 B |
249
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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
participer.thsf.net/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');
|
BIN
participer.thsf.net/upload/4pattes.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
participer.thsf.net/upload/what_you_ride.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
participer.thsf.net/upload/what_you_ride_final.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
@ -207,7 +207,6 @@ function get_value_from_ajax(obj, url, err_code) {
|
||||
}
|
||||
};
|
||||
xhttp.open('POST', url, true);
|
||||
alert(xhttp.readyState);
|
||||
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhttp.send();
|
||||
}
|
||||
|