Browse Source

Initial commit

master
Doug Le Tough 3 years ago
commit
99181ffa08
35 changed files with 1473 additions and 0 deletions
  1. +4
    -0
      .gitignore
  2. +3
    -0
      config.local.py
  3. +1
    -0
      config.py
  4. BIN
      static/fonts/RobotoCondensed-Bold.ttf
  5. BIN
      static/fonts/RobotoCondensed-Regular.ttf
  6. BIN
      static/images/404.png
  7. BIN
      static/images/add.png
  8. BIN
      static/images/dummy_pic.png
  9. BIN
      static/images/edit.png
  10. BIN
      static/images/favicon.png
  11. BIN
      static/images/login.png
  12. BIN
      static/images/logo.png
  13. BIN
      static/images/logout.png
  14. BIN
      static/images/refresh.png
  15. BIN
      static/images/save.png
  16. BIN
      static/images/search.png
  17. BIN
      static/images/trash.png
  18. BIN
      static/images/upload.png
  19. +249
    -0
      static/scripts/tetawebapp.js
  20. +29
    -0
      static/styles/colors.css
  21. +20
    -0
      static/styles/fonts.css
  22. +370
    -0
      static/styles/tetawebapp.css
  23. +49
    -0
      templates/ajax.html
  24. +24
    -0
      templates/ajax_html.html
  25. +12
    -0
      templates/articles.html
  26. +65
    -0
      templates/articles_by_id.html
  27. +68
    -0
      templates/basics.html
  28. +12
    -0
      templates/database.html
  29. +15
    -0
      templates/error.html
  30. +106
    -0
      templates/index.html
  31. +53
    -0
      templates/inputs.html
  32. +23
    -0
      templates/login.html
  33. +18
    -0
      templates/todo.html
  34. +304
    -0
      tetawebapp.py
  35. +48
    -0
      tetawebapp.sql

+ 4
- 0
.gitignore View File

@ -0,0 +1,4 @@
*un~
*.swp
*.pyc
*.wsgi

+ 3
- 0
config.local.py View File

@ -0,0 +1,3 @@
SQLALCHEMY_TRACK_MODIFICATIONS = True
SQLALCHEMY_DATABASE_URI = "postgresql://tetawebapp:tetawebapp@localhost/tetawebapp"
UPLOADED_FILES_DEST = "./upload"

+ 1
- 0
config.py View File

@ -0,0 +1 @@
config.local.py

BIN
static/fonts/RobotoCondensed-Bold.ttf View File


BIN
static/fonts/RobotoCondensed-Regular.ttf View File


BIN
static/images/404.png View File

Before After
Width: 320  |  Height: 240  |  Size: 133 KiB

BIN
static/images/add.png View File

Before After
Width: 16  |  Height: 16  |  Size: 386 B

BIN
static/images/dummy_pic.png View File

Before After
Width: 400  |  Height: 200  |  Size: 7.2 KiB

BIN
static/images/edit.png View File

Before After
Width: 16  |  Height: 16  |  Size: 938 B

BIN
static/images/favicon.png View File

Before After
Width: 35  |  Height: 35  |  Size: 2.3 KiB

BIN
static/images/login.png View File

Before After
Width: 16  |  Height: 16  |  Size: 321 B

BIN
static/images/logo.png View File

Before After
Width: 64  |  Height: 64  |  Size: 3.2 KiB

BIN
static/images/logout.png View File

Before After
Width: 16  |  Height: 16  |  Size: 327 B

BIN
static/images/refresh.png View File

Before After
Width: 16  |  Height: 16  |  Size: 312 B

BIN
static/images/save.png View File

Before After
Width: 16  |  Height: 16  |  Size: 357 B

BIN
static/images/search.png View File

Before After
Width: 16  |  Height: 16  |  Size: 371 B

BIN
static/images/trash.png View File

Before After
Width: 16  |  Height: 16  |  Size: 878 B

BIN
static/images/upload.png View File

Before After
Width: 16  |  Height: 16  |  Size: 244 B

+ 249
- 0
static/scripts/tetawebapp.js View 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
- 0
static/styles/colors.css View 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
- 0
static/styles/fonts.css View 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
- 0
static/styles/tetawebapp.css View 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
- 0
templates/ajax.html View 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
- 0
templates/ajax_html.html View 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
- 0
templates/articles.html View 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
- 0
templates/articles_by_id.html View 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
- 0
templates/basics.html View 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
- 0
templates/database.html View 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
- 0
templates/error.html View 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
- 0
templates/index.html View 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
- 0
templates/inputs.html View 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
- 0
templates/login.html View 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
- 0
templates/todo.html View 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
- 0
tetawebapp.py View 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
- 0
tetawebapp.sql View 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');

Loading…
Cancel
Save