Initial commit

这个提交包含在:
Doug Le Tough 2018-02-26 10:41:52 +01:00
当前提交 99181ffa08
共有 35 个文件被更改,包括 1473 次插入0 次删除

4
.gitignore vendored 普通文件
查看文件

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

3
config.local.py 普通文件
查看文件

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

1
config.py 符号链接
查看文件

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

二进制
static/fonts/RobotoCondensed-Bold.ttf 普通文件

二进制文件未显示。

二进制文件未显示。

二进制
static/images/404.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 133 KiB

二进制
static/images/add.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 386 B

二进制
static/images/dummy_pic.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 7.2 KiB

二进制
static/images/edit.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 938 B

二进制
static/images/favicon.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 2.3 KiB

二进制
static/images/login.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 321 B

二进制
static/images/logo.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 3.2 KiB

二进制
static/images/logout.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 327 B

二进制
static/images/refresh.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 312 B

二进制
static/images/save.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 357 B

二进制
static/images/search.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 371 B

二进制
static/images/trash.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 878 B

二进制
static/images/upload.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 244 B

249
static/scripts/tetawebapp.js 普通文件
查看文件

@ -0,0 +1,249 @@
var red = "#FF0000";
var green = "#00FF00";
var light_red = "#FCD5DC";
var light_green = "#D5FCD8";
var base_bg = "#FFFFFF";
var base_border = "#888888";
var coloured_bg = "#FF5D00";
var clear_bg = "#E5E5E5";
var text_color = "#555555";
/* **************************************************************************************
* GLOBAL
* **************************************************************************************/
// Cookies
function setcookie(cname, cvalue, exdays) {
// Set cookie
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function getcookie(cname) {
// Get cookie by name
var value = "; " + document.cookie;
var parts = value.split("; " + cname + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
// Eye candies
function valid_input(obj) {
// Valid input makes obj background to glow green for 2 seconds
// If obj borders were red, they get they normal color back
obj.style.backgroundColor = light_green;
obj.style.borderColor = base_border;
setTimeout( function() {
obj.style.backgroundColor = base_bg;
}
, 2000);
}
function invalid_input(obj) {
// Invalid input makes obj borders and background to glow red for 2 seconds
// Border color will stay red until a valid input is sent
obj.style.backgroundColor = light_red;
obj.style.borderColor = red;
setTimeout( function() {
obj.style.backgroundColor = base_bg;
}
, 2000);
}
function valid_upload(obj) {
// Valid input makes obj background to glow green for 2 seconds
// If obj borders were red, they get they normal color back
obj.style.backgroundColor = green;
obj.style.borderColor = text_color;
obj.style.borderStyle = 'solid';
setTimeout( function() {
obj.style.backgroundColor = clear_bg;
obj.style.borderStyle = 'none';
}
, 2000);
}
function invalid_upload(obj) {
// Invalid input makes obj borders and background to glow red for 2 seconds
// Border color will stay red until a valid input is sent
obj.style.backgroundColor = red;
obj.style.borderColor = text_color;
obj.style.borderStyle = 'solid';
setTimeout( function() {
obj.style.borderStyle = 'solid';
obj.style.backgroundColor = clear_bg;
obj.style.borderColor = red;
}
, 2000);
}
function lit(obj) {
// Lit bacground and border on obj (use by input type=file)
obj.style.backgroundColor = coloured_bg;
obj.style.borderColor = text_color;
obj.style.borderStyle = 'solid';
}
function unlit(obj) {
// Unlit bacground and border on obj (use by input type=file)
obj.style.backgroundColor = clear_bg;
obj.style.borderColor = clear_bg;
obj.style.borderStyle = 'none';
}
function verify_login() {
// Verify login inputs
login = document.getElementById('login');
password = document.getElementById('password');
if (login.value.length > 0) {
valid_input(login);
if (password.value.length > 0) {
valid_input(password);
return true;
}
invalid_input(password);
return false;
}
invalid_input(login);
return false;
}
function logout() {
// Logout user
setcookie('token', '', 30);
document.location = '/';
}
/* **************************************************************************************
* AJAX
* **************************************************************************************/
function get_html_from_ajax(obj, url) {
// Get HTML content from AJAX request from url argument
// HTML content is then put as innerHTML to obj
var xhttp = new XMLHttpRequest();
xhttp.onerror = function(){
obj.innerHTML = "Error while getting content (1)";
};
xhttp.onload = function(){
if (xhttp.status != 200) {
obj.innerHTML = "Error while getting content (2)";
}
};
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var response = xhttp.responseText;
obj.innerHTML = response;
}
};
xhttp.open('POST', url, true);
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.send();
}
function set_value_from_ajax(obj, url, err_code) {
// Send value from obj.value via AJAX request to url argument
// obj.value is passed to URL in a REST sheme like <URL>/<VALUE>
// If err_code response is received, then a server side
// error has occured and input is invalidated.
url = url + '/' + obj.value;
var xhttp = new XMLHttpRequest();
xhttp.onerror = function(){
invalid_input(obj);
};
xhttp.onload = function(){
if (xhttp.status != 200) {
invalid_input(obj);
}
};
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var response = xhttp.responseText;
if (response == err_code) {
invalid_input(obj);
return;
}
valid_input(obj);
return;
}
};
xhttp.open('POST', url, true);
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.send();
}
function get_value_from_ajax(obj, url, err_code) {
// Get value from AJAX request
// The returned value is then set to obj.value.
// If err_code response is received, then a server side
// error has occured and input is invalidated
var xhttp = new XMLHttpRequest();
xhttp.onerror = function(){
invalid_input(obj);
};
xhttp.onload = function(){
if (xhttp.status != 200) {
invalid_input(obj);
}
};
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var response = xhttp.responseText;
if (response == err_code) {
invalid_input(obj);
return;
}
obj.value = response;
valid_input(obj);
return;
}
};
xhttp.open('POST', url, true);
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.send();
}
function upload_file_from_ajax(obj, url, err_code) {
// Upload files get from <obj> to the specified <url>
// if <errcode> is returned input is invalidated
var files = obj.files;
var icon_id = obj.id.substring(obj.id.lastIndexOf("_") + 1);
var icon_obj = document.getElementById("upload_icon_" + icon_id)
var xhttp = new XMLHttpRequest();
xhttp.onerror = function(){
invalid_upload(icon_obj);
};
xhttp.onload = function(){
if (xhttp.status != 200) {
invalid_upload(icon_obj);
}
};
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var response = xhttp.responseText;
if (response == err_code) {
invalid_upload(icon_obj);
return;
}
valid_upload(icon_obj);
return;
}
};
xhttp.open('POST', url, true);
var formData = new FormData();
for (var i=0; i < files.length; i++){
formData.append("files", files[i], files[i].name);
}
xhttp.send(formData);
}

29
static/styles/colors.css 普通文件
查看文件

@ -0,0 +1,29 @@
/*
* Here are the base color scheme and icon set.
* You can modify it or create your own using the same variables
* and make it loaded after this one but before the fonts.css in
* the HTML header section of the index.html template file.
*/
:root {
--coloured-bg: #FF5D00;
--light-coloured-bg: #FFB387;
--clear-bg: #E5E5E5;
--mid-bg: #BBBBBB;
--dark-bg: #2B2B2B;
--dark-border: #888888;
--text-color: #555555;
--white: #FFFFFF;
--black: #000000;
--font-normal: url("/static/fonts/RobotoCondensed-Regular.ttf") format("truetype");
--font-bold: url("/static/fonts/RobotoCondensed-Bold.ttf") format("truetype");
--banner-logo: url(/static/images/logo.png);
--add_icon: url(/static/images/add.png);
--edit_icon: url(/static/images/edit.png);
--login_icon: url(/static/images/login.png);
--logout_icon: url(/static/images/logout.png);
--refresh_icon: url(/static/images/refresh.png);
--save_icon: url(/static/images/save.png);
--search_icon: url(/static/images/search.png);
--trash_icon: url(/static/images/trash.png);
--upload_icon: url(/static/images/upload.png);
}

20
static/styles/fonts.css 普通文件
查看文件

@ -0,0 +1,20 @@
/*
* Here are the font definitions.
* You can modify it or create your own and make it loaded
* after this one in the HTML header section of the index.html
* template file.
*/
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 400;
src: var(--font-normal);
}
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 700;
src: var(--font-bold);
}

370
static/styles/tetawebapp.css 普通文件
查看文件

@ -0,0 +1,370 @@
/*
* Do NOT modify this file:
* ------------------------
* If you want to add or modify classes, create a new
* CSS files and make it loaded after this one in the
* HTML header section of the index.html template file.
*/
* {
box-sizing: border-box;
}
body {
margin: 10px;
font-family: "Roboto Condensed";
background-color: var(--dark-bg);
}
div.content {
display: flex;
min-height: calc(100vh - 110px);
}
main > article {
flex: 1;
background-color: var(--clear-bg);
}
main > section.inline {
display: flex;
background-color: var(--clear-bg);
}
main > section.inline > article.left {
flex: 0 0 50%;
margin-left: 10px;
}
main > section.inline > article.right {
flex: 1;
margin-left: 10px;
}
div.content > nav.vertical {
flex: 0 0 200px;
background-color: var(--clear-bg);
border-right-color: var(--mid-bg);
border-right-style: solid;
border-right-width: 1px;
}
div.content > nav.vertical {
order: -1;
display: block;
}
div.content > nav.vertical > a {
display: block;
background-color: var(--clear-bg);
font-size: 20px;
color: var(--text-color);
padding: 5px;
text-decoration: none;
}
div.content > nav.vertical > a:hover {
background-color: var(--coloured-bg);
color: var(--white);
cursor: pointer;
}
div.content > nav.vertical > a.selected {
background-color: var(--light-coloured-bg);
}
main {
color: var(--text-color);
background-color: var(--clear-bg);
width: 100%;
}
main > div.navbar_container {
text-align: center;
padding: 0;
margin: 0;
}
main > div.navbar_container > ul.horizontal {
display: inline-block;
list-style-type: none;
margin: 10px;
padding: 0;
overflow: hidden;
background-color: var(--white);
border-color: var(--coloured-bg);
border-style: solid;
border-width: 1px;
color: var(--text-color);
border-radius: 2px;
}
main > div.navbar_container > ul.horizontal > li {
float: left;
}
main > div.navbar_container > ul.horizontal > li > a {
display: block;
color: var(--text-color);
text-align: center;
padding: 5px;
text-decoration: none;
}
main > div.navbar_container > ul.horizontal > li > a:hover {
background-color: var(--coloured-bg);
color: var(--white);
}
main > div.navbar_container > ul.horizontal > li > a.right_border {
border-right-color: var(--coloured-bg);
border-right-style: solid;
border-right-width: 1px;
}
main > div.navbar_container > ul.horizontal > li > a.selected {
background-color: var(--light-coloured-bg);
}
main > article {
padding: 10px;
color: var(--text-color);
display: block;
}
main > article.error,
main > article.error > p {
padding: 10px;
color: var(--text-color);
display: block;
text-align: center;
}
main > article > h3,
main > section.inline > article > h3 {
font-size: 30px;
color: var(--text-color);
margin-bottom: 10px;
}
main > article > p,
main > article > ul,
main > article > ol {
color: var(--text-color);
text-align: justify;
text-justify: distribute;
}
main > hr {
border-color: var(--mid-bg);
border-style: solid;
border-width: 1px;
}
main > article > img {
display:inline-block;
border-color: var(--mid-bg);
border-style: solid;
border-width: 1px;
border-radius: 4px;
}
main > article > p > a {
color: var(--coloured-bg);
}
main > article > p > a:hover {
text-decoration: none;
}
main > article.right > img {
float: right;
margin: 0 0 0px 10px;
}
main > article.left > img {
float: left;
margin: 0 10px 0px 0;
}
header {
height: 65px;
font-size: 34px;
padding: 10px;
text-align: right;
color: var(--white);
background: var(--banner-logo);
background-repeat: no-repeat;
background-position: 10px;
text-shadow: 0 0 1px var(--black);
border-bottom-color: var(--dark-border);
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-color: var(--white);
border-top-style: solid;
border-top-width: 1px;
}
footer {
height: 35px;
font-size: 12px;
text-align: center;
padding: 1em;
border-bottom-color: var(--white);
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-color: var(--dark-border);
border-top-style: solid;
border-top-width: 1px;
}
header,
footer {
background-color: var(--coloured-bg);
color: var(--white);
}
input[type="text"],
input[type="password"],
textarea,
select,
pre {
border-color: var(--dark-border);
border-style: solid;
border-width: 1px;
background-color: var(--white);
color: var(--text-color);
padding: 5px;
font-family: "Roboto Condensed";
margin: 5px;
}
pre {
border-color: var(--coloured-bg);
}
button,
input[type="button"],
input[type="submit"] {
border-color: var(--dark-border);
border-style: solid;
border-width: 1px;
background-color: var(--coloured-bg);
color: var(--white);
font-weight: bold;
padding: 5px;
font-family: "Roboto Condensed";
margin: 5px;
border-radius: 4px;
}
button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
input[type="file"]:hover {
background-color: var(--light-coloured-bg);
color: var(--text-color);
cursor: pointer;
}
div.file_upload {
display: inline-block;
position: relative;
width: 20px;
height: 20px;
margin: 0;
padding: 0;
border-radius: 2px;
border-style: solid;
border-width: 1px;
border-color: var(--clear-bg);
}
input[type="file"] {
position: absolute;
width: 18px;
height: 18px;
left: 0;
top: 1px;
opacity: 0;
}
input.add,
input.edit,
input.login,
input.logout,
input.refresh,
input.save,
input.search,
input.trash,
input.upload {
width: 20px;
height: 20px;
margin: 0;
padding: 0;
border-radius: 2px;
border-style: none;
}
input.add:hover,
input.edit:hover,
input.login:hover,
input.logout:hover,
input.refresh:hover,
input.save:hover,
input.search:hover,
input.trash:hover,
input.upload:hover {
border-color: var(--text-color);
border-style: solid;
border-width: 1px;
background-color: var(--coloured-bg);
cursor: pointer;
}
input.add {
background: var(--add_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.edit {
background: var(--edit_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.login {
background: var(--login_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.logout {
background: var(--logout_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.refresh {
background: var(--refresh_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.save {
background: var(--save_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.search {
background: var(--search_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.trash {
background: var(--trash_icon);
background-repeat: no-repeat;
background-position: center center;
}
input.upload {
background: var(--upload_icon);
background-repeat: no-repeat;
background-position: center center;
}

49
templates/ajax.html 普通文件
查看文件

@ -0,0 +1,49 @@
{% extends "index.html" %}
{% block title %}Ajax{% endblock %}
{% block main %}
<section class='inline'>
<article class='left'>
<h3>Get HTML response from AJAX</h3>
<p>Click the refresh button to get the HTML response.</p>
<p>The response may randomly be a voluntary error so you should try it more than once.</p>
Refresh: <input type='button' class='refresh' value=' '
onclick='javascript:get_html_from_ajax(document.getElementById("html_container"), "/get_html_from_ajax");'>
</article>
<article class='right'>
<h3>Upload files with AJAX</h3>
<p>Select files to upload</p>
<p>The response may randomly be a voluntary error so you should try it more than once.</p>
Upload files:
<div class='file_upload'>
<!--
Input file is a tricky hack (see tetawebapp.css and tetawebapp.js)
-->
<input type='button' id='upload_icon_1' class='upload' title='Upload' value=' '/>
<input type='file' id='upload_input_1' multiple
title='Upload'
onchange='javascript:upload_file_from_ajax(this, "/upload", "TETA_ERR");'
onmouseover='javascript:lit(document.getElementById("upload_icon_1"));'
onmouseout='javascript:unlit(document.getElementById("upload_icon_1"));'/>
</div>
</article>
</section>
<hr/>
<article class='right' id='html_container'></article>
<hr/>
<section class='inline'>
<article class='left'>
<h3>Set value via AJAX</h3>
<p>Send value to the application.</p>
<p>If value is empty or is "We Make Porn" (case sensitive), an error is raised.</p>
<input type='text' id='value_sender'>
<input type='button' value="Try me" onclick='javascript:set_value_from_ajax(document.getElementById("value_sender"), "/set_value_from_ajax", "TETA_ERR");'>
</article>
<article class='right'>
<h3>Get value from AJAX</h3>
<p>Get a random value from the application.</p>
<p>Randomly raises a voluntary error so you should try it more than once.</p>
<input type='text' id='value_receiver'>
<input type='button' value="Try me" onclick='javascript:get_value_from_ajax(document.getElementById("value_receiver"), "/get_value_from_ajax", "TETA_ERR");'>
</article>
</section>
{% endblock %}

24
templates/ajax_html.html 普通文件
查看文件

@ -0,0 +1,24 @@
<h3>This is the title</h3>
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore
</p>
<p>This <a href='/plop.html'>link</a> will lead to an error page</p>
<p>
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia desers unt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>

12
templates/articles.html 普通文件
查看文件

@ -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 %}

查看文件

@ -0,0 +1,65 @@
{% extends "index.html" %}
{% block title %}Articles{% endblock %}
{% block main %}
<article class='right'>
<h3>Article #{{ ID }}</h3>
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore
</p>
<p>This <a href='/plop.html'>link</a> will lead to an error page</p>
<p>
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia desers unt mollit anim id est laborum.
</p>
<ul>
<li>plop</li>
<li>plap</li>
<li>plip</li>
</ul>
<ol>
<li>plop</li>
<li>plap</li>
<li>plip</li>
</ol>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
</article>
<article class='left'>
<h3>Another disposition</h3>
<img src='/static/images/dummy_pic.png' alt='dummy pic' title='dummy pic'/>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore
</p>
<p>
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
</p>
</article>
{% endblock %}

68
templates/basics.html 普通文件
查看文件

@ -0,0 +1,68 @@
{% extends "index.html" %}
{% block title %}Basics{% endblock %}
{% block main %}
<article class='right'>
<h3>Basics</h3>
<p>
Thanks to <a href='http://flask.pocoo.org/'>Python/Flask</a> with <strong>TetaWebApp</strong> most of the output things come to life via
<a href='http://jinja.pocoo.org/docs/2.10/'>Jinja2 HTML templates</a>
and is 100% <strong title='Bulshit inside'>HTML5 ready©</strong>.
</p>
<p>
Colors and fonts are managed from separated CSS files letting you easily
change the default theme to your favorite colors and icon set.
</p>
<pre>
/*
* Here are the font definitions.
* You can modify it or create your own and make it loaded
* after this one in the HTML header section of the index.html
* template file.
*/
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 400;
src: var(--font-normal);
}
@font-face {
font-family: "Roboto Condensed";
font-style: normal;
font-weight: 700;
src: var(--font-bold);
}
</pre>
<pre>
/*
* Here are the base color scheme and icon set.
* You can modify it or create your own using the same variables
* and make it loaded after this one but before the fonts.css in
* the HTML header section of the index.html template file.
*/
:root {
--coloured-bg: #FF5D00;
--light-coloured-bg: #FFB387;
--clear-bg: #E5E5E5;
--mid-bg: #BBBBBB;
--dark-bg: #2B2B2B;
--dark-border: #888888;
--text-color: #555555;
--white: #FFFFFF;
--black: #000000;
--font-normal: url("/static/fonts/RobotoCondensed-Regular.ttf") format("truetype");
--font-bold: url("/static/fonts/RobotoCondensed-Bold.ttf") format("truetype");
--banner-logo: url(/static/images/logo.png);
--add_icon: url(/static/images/add.png);
--edit_icon: url(/static/images/edit.png);
--login_icon: url(/static/images/login.png);
--logout_icon: url(/static/images/logout.png);
--refresh_icon: url(/static/images/refresh.png);
--save_icon: url(/static/images/save.png);
--search_icon: url(/static/images/search.png);
--trash_icon: url(/static/images/trash.png);
}
</pre>
</article>
{% endblock %}

12
templates/database.html 普通文件
查看文件

@ -0,0 +1,12 @@
{% extends "index.html" %}
{% block title %}Database{% endblock %}
{% block main %}
<article class='right'>
<h3>Accessing database</h3>
<p>
Even if using <a href='http://flask-sqlalchemy.pocoo.org/2.3/'>Flask-SQLAlchemy</a> to retrieve data
stored in <strong>Postgres</strong> databases is the recommended way to use <strong>TetaWebApp</strong>,
you're free to use the database connector that suits your need.
</p>
</article>
{% endblock %}

15
templates/error.html 普通文件
查看文件

@ -0,0 +1,15 @@
{% extends "index.html" %}
{% block title %}Erreur{% endblock %}
{% block nav %}{% endblock %}
{% block main %}
<article class='error'>
<h3>404 - Not found</h3>
<p>The page you asked for was not found on this server.<br/>
It may has been lost, it may has never existed.<br/>
Maybe we don't care at all...</p>
<p>
<input type='button' value='Get me back to business' onclick='javascript:document.location="/";'/>
</p>
<img src='/static/images/404.png' alt='404 - Not found' title='404 - Not found'/>
</article>
{% endblock %}

106
templates/index.html 普通文件
查看文件

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang='zxx'>
<head>
<title>TetaWebApp - {% block title %}Accueil{% endblock %}</title>
<meta name="viewport" content="initial-scale=1.0" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="/static/styles/colors.css" />
<link rel="stylesheet" type="text/css" href="/static/styles/fonts.css" />
<link rel="stylesheet" type="text/css" href="/static/styles/tetawebapp.css" />
<link rel="icon" type="image/png" href="/static/images/favicon.png" />
<script src="/static/scripts/tetawebapp.js"></script>
</head>
{% block bodyheader %}
<body>
{% endblock %}
<header>{% block banner %}TetaWebApp{% endblock %}</header>
<div class='content'>
{% block nav %}
<nav class='vertical'>
{% block menu %}
{% for item in menu %}
{% for key in item[1] %}
{% if item[2] == 1 %}
<a class='selected' href='{{ key }}'>{{ item[0] }}</a>
{% else %}
<a href='{{ key }}'>{{ item[0] }}</a>
{% endif %}
{% endfor %}
{% endfor %}
{% endblock %}
</nav>
{% endblock%}
<main>
{% if navbar %}
<div class='navbar_container'>
<ul class='horizontal'>
{% for item in navbar %}
{% set selected = ['', 'selected'] %}
{% set last = ['right_border', 'last'] %}
{% for url in item[1] %}
<li><a class='{{ last[item[3]] }} {{ selected[item[2]] }}' href='{{ url }}'>{{ item[0] }}</a></li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
{% block main %}
<article class='right'>
<h3>TetaWebApp demo</h3>
<p>
Welcome to the <strong>TetaWebApp</strong> demo
</p>
<p>
TetaWebApp is a basic web application template based on <a href='http://flask.pocoo.org/'>Python/Flask</a>
and <a href='https://www.w3schools.com/js/js_ajax_intro.asp'>AJAX</a> made by
<a href='mailto:doug.letough@free.fr'>Doug Le Tough</a> from <a href='https://www.tetalab.org'>Tetalab</a>.
</p>
<p>
The goal of this project is to provide a basic framework to make any web application you need while
letting you complete freedom on how to use or extend it <strong>without</strong> using any Google,
Bootstrap or any other piece of <strong>shitty free spyware</strong>.
</p>
TetaWebApp will <strong>never</strong> download or upload anything in any way.
<p>
</p>
<p>There is <strong>no</strong> limitation, you can use all or only parts of <strong>TetaWebApp</strong>
and you can <strong title='bullshit inside'>virtually</strong> do any app you want with TetaWebApp.
</p>
<p>
But be sure that freedom has a cost: You <strong>will</strong> need work to make it work ;-)
</p>
<p>
<strong>TetaWebApp</strong> is released under the only real <strong>free</strong> license: The
<a href='http://www.wtfpl.net/'><img src='http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-2.png'
title='WTFPL' alt='WTFPL' /></a>.
</p>
<pre>
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
</pre>
<p>
Get a copy of <strong>TetaWebApp</strong>:<br/>
<pre>
git clone git://git.tetalab.org/tetalab/tetawebapp
</pre>
</p>
</article>
{% endblock %}
</main>
</div>
{% block footer %}
<footer>© - Tetalab - Le hacker space Toulousaing' putaing' cong' -</footer>
{% endblock%}
</body>
</html>

53
templates/inputs.html 普通文件
查看文件

@ -0,0 +1,53 @@
{% extends "index.html" %}
{% block title %}Inputs{% endblock %}
{% block main %}
<article class='right'>
<h3>The input collection</h3>
<p>
Have a look to the input collection:
</p>
<input type='text'/>
<button>Click me</button>
<br/>
<textarea cols='25'></textarea>
<br/>
<select>
{% for item in menu %}
<option>{{ item[0] }}</option>
{% endfor %}
</select>
<input type='submit' value='Click me too'/>
<br/>
<input type='button' value='And me !' />
<br/>
<input type='button' class='add' title='Add' value=' '/>
<input type='button' class='edit' title='Edit' value=' '/>
<input type='button' class='login' title='Login' value=' '/>
<input type='button' class='logout' title='Logout' value=' ' onclick='javascript:logout();'/>
<input type='button' class='refresh' title='Refresh' value=' '/>
<input type='button' class='save' title='Save' value=' '/>
<input type='button' class='search' title='Search' value=' '/>
<input type='button' class='trash' title='Trash' value=' '/>
<!--
Input file is a tricky hack (see tetawebapp.css and tetawebapp.js)
-->
<div class='file_upload'>
<input type='button' id='upload_icon_1' class='upload' title='Upload' value=' '/>
<input type='file' id='upload_input_1' name='files' multiple
title='Upload'
onchange='javascript:upload_file_from_ajax(this, "/upload", "TETA_ERR");'
onmouseover='javascript:lit(document.getElementById("upload_icon_1"));'
onmouseout='javascript:unlit(document.getElementById("upload_icon_1"));'/>
</div>
<br/>
<pre>
#!/bin/sh
# This is code sample
while [ 1 ]
do
echo "Tits or GTFO !"
sleep .1
done
</pre>
</article>
{% endblock %}

23
templates/login.html 普通文件
查看文件

@ -0,0 +1,23 @@
{% extends "index.html" %}
{% block title %}Login{% endblock %}
{% block nav %}{% endblock %}
{% block main %}
<article class='login'>
<h3>Login</h3>
<p>The demo login is:</p>
<ul>
<li>Login: demo</li>
<li>Password: demo</li>
</ul>
</article>
{% if message != '' %}
<pre>{{ message }}</pre>
{% endif %}
<article class='left'>
<form method='POST' action='/login'>
Login: <input id='login' name='login' type='text' />
Password: <input id='password' name='password' type='password' />
<input type='submit' value='Log me in' onclick='javascript:return verify_login();'>
</form>
</article>
{% endblock %}

18
templates/todo.html 普通文件
查看文件

@ -0,0 +1,18 @@
{% extends "index.html" %}
{% block title %}TODO{% endblock %}
{% block main %}
<article class='right'>
<h3>TODO list</h3>
<ul>
<li><strike>Basic menu management</strike></li>
<li>Installation wizard</li>
<li>Back office for basic content management</li>
<li><strike>Basic Ajax support</strike></li>
<li><strike>Session management</strike></li>
<li>File upload</li>
<li>Basic documentation</li>
<li><strike>Horizontal navbar</strike></li>
<li><strike>License</strike></li>
</ul>
</article>
{% endblock %}

304
tetawebapp.py 可执行文件
查看文件

@ -0,0 +1,304 @@
#!/usr/bin/env python
# -*- coding: utf-8
# Required modules
import os
import inspect
import random
import binascii
import bcrypt
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
from functools import wraps
# Optionnal modules
import psycopg2
from flask_sqlalchemy import SQLAlchemy
########################################################################
# App settings
########################################################################
app = Flask(__name__)
# Path to static files
app.static_url_path='/static'
# Set debug mode to False for production
app.debug = True
# Various configuration settings belong here (optionnal)
app.config.from_pyfile('config.local.py')
# Generate a new key: head -n 40 /dev/urandom | md5sum | cut -d' ' -f1
app.secret_key = 'ce1d1c9ff0ff388a838b3a1e3207dd27'
# Feel free to use SQLAlchemy (or not)
db = SQLAlchemy(app)
########################################################################
# Sample user database
########################################################################
class Tetawebapp_users(db.Model):
__tablename__ = 'tetawebapp_users'
id = db.Column(db.Integer, primary_key=True)
mail = db.Column(db.Text, nullable=False)
password = db.Column(db.Text, nullable=False)
name = db.Column(db.Text, nullable=False)
########################################################################
# Menu and navigation management
########################################################################
def get_menu(page):
""" The main menu is a list of lists in the followin format:
[unicode caption,
{unicode URL endpoint: [unicode route, ...]},
int 0]
- The URL end point is the URL where to point to (href)
- One of the routes MUST match the route called by request
- The int 0 is used to determine which menu entry is actally called.
The value MUST be 0."""
menu = [[u'Home', {u'/': [u'/']}, 0],
[u'Articles', {u'/articles': [u'/articles', u'/articles/<ID>']}, 0],
[u'Basics', {u'/basics': [u'/basics']}, 0],
[u'Inputs', {u'/inputs': [u'/inputs']}, 0],
[u'Ajax', {u'/ajax': [u'/ajax']}, 0],
[u'Database', {u'/database': [u'/database']}, 0],
[u'Todo', {u'/todo': [u'/todo']}, 0],
]
for item in menu:
for url in item[1]:
for route in item[1][url]:
if route == page:
item[2] = 1
return menu
# This should never happen
return menu
def get_navbar(page, selected):
""" The horizontal navbar is a list of lists in the followin format:
[unicode caption, {unicode URL endpoint: [unicode route, ...]}, int 0]
- The URL end point is the URL where to point to (href)
- One of the routes MUST match the route called by request
- The int 0 is used to de """
navbars = [[u'First article', {u'/articles/1': [u'/articles', u'/articles/<ID>']}, 0, 0],
[u'Second article', {u'/articles/2': [u'/articles', u'/articles/<ID>']}, 0, 0],
[u'Third article', {u'/articles/3': [u'/articles', u'/articles/<ID>']}, 0, 0]
]
navbar = []
for item in navbars:
for url in item[1]:
if url == selected:
item[2] = 1
for route in item[1][url]:
if route == page:
navbar.append(item)
navbar[len(navbar) - 1][3] = 1
return navbar
########################################################################
# Session management
########################################################################
def sync_session(request, session):
""" Synchronize cookies with session """
for key in request.cookies:
session[key] = request.cookies[key].encode('utf8')
def sync_cookies(response, session):
""" Synchronize session with cookies """
for key in session:
response.set_cookie(key, value=str(session[key]))
def check_session(func):
""" Check if the session has required token cookie set.
If not, redirects to the login page. """
@wraps(func)
def check(*args, **kwargs):
try:
if session['token'] == request.cookies['token'] and len(session['token']) > 0:
return func(*args, **kwargs)
else:
session['token'] = ''
response = app.make_response(render_template('login.html', message=''))
sync_cookies(response, session)
return response
except KeyError:
return render_template('login.html', message='')
return check
def check_login(login, password):
""" Puts the login verification code here """
password = password.encode('utf-8')
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
stored_hash = Tetawebapp_users.query.filter_by(mail=login).with_entities(Tetawebapp_users.password).first()
if stored_hash:
if bcrypt.checkpw(password, stored_hash[0].encode('utf-8')):
return True
return False
def gen_token():
""" Generate a random token to be stored in session and cookie """
token = binascii.hexlify(os.urandom(42))
return token
########################################################################
# Routes:
# -------
# Except for the index function, the function name MUST have the same
# name than the URL endpoint to make the menu work properly
########################################################################
@app.errorhandler(404)
def page_not_found(e):
""" 404 not found """
return render_template('error.html'), 404
@app.route("/login", methods=['GET', 'POST'])
def login():
login = request.form.get('login')
password = request.form.get('password')
if check_login(login, password):
# Generate and store a token in session
session['token'] = gen_token()
# Return user to index page
page = '/'
menu = get_menu(page)
response = app.make_response(render_template('index.html', menu=menu))
# Push token to cookie
sync_cookies(response, session)
return response
# Credentials are not valid
response = app.make_response(render_template('login.html', message='Invalid user or password'))
session['token'] = ''
sync_cookies(response, session)
return response
@app.route("/", methods=['GET', 'POST'])
@check_session
def index():
""" Index page """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('index.html', menu=menu)
@app.route("/articles", methods=['GET', 'POST'])
@check_session
def articles():
""" Arcticles page """
page = str(request.url_rule)
menu = get_menu(page)
navbar = get_navbar(page, '')
return render_template('articles.html', menu=menu, navbar=navbar)
@app.route("/articles/<ID>", methods=['GET', 'POST'])
@check_session
def articles_by_id(ID):
""" Arcticles page """
page = str(request.url_rule)
menu = get_menu(page)
selected = page.replace('<ID>', ID)
navbar = get_navbar(page, selected)
return render_template('articles_by_id.html', menu=menu, navbar=navbar, ID=ID)
@app.route("/basics", methods=['GET', 'POST'])
@check_session
def basics():
""" Basics page """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('basics.html', menu=menu)
@app.route("/inputs", methods=['GET', 'POST'])
@check_session
def inputs():
""" Show the input collection """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('inputs.html', menu=menu)
@app.route("/ajax", methods=['GET', 'POST'])
@check_session
def ajax():
""" Propose various AJAX tests """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('ajax.html', menu=menu)
@app.route("/database", methods=['GET', 'POST'])
@check_session
def database():
""" A blah on using databases """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('database.html', menu=menu)
@app.route("/todo", methods=['GET', 'POST'])
@check_session
def todo():
""" The famous TODO list """
page = str(request.url_rule)
menu = get_menu(page)
return render_template('todo.html', menu=menu)
########################################################################
# AJAX routes
########################################################################
@app.route("/get_html_from_ajax", methods=['GET', 'POST'])
@check_session
def get_html_from_ajax():
""" Return HTML code to an AJAX request
It may generate a 404 http error for testing purpose """
if int(random.random()*10) % 2:
# Randomly generate 404 HTTP response
return render_template('error.html'), 404
return render_template('ajax_html.html')
@app.route("/get_value_from_ajax", methods=['GET', 'POST'])
@check_session
def get_value_from_ajax():
""" Return a randomly generated value to an AJAX request
It may return an error code for testing purpose """
err_code = 'TETA_ERR'
RND = int(random.random()*10)
if RND % 2:
# Randomly generate error
return err_code
return str(RND)
@app.route("/set_value_from_ajax/<value>", methods=['GET', 'POST'])
@check_session
def set_value_from_ajax(value):
""" Accept a value from an AJAX request
It may return an error code for testing purpose """
err_code = 'TETA_ERR'
if value != 'We Make Porn':
return 'True'
return err_code
@app.route("/upload", methods=['POST'])
@check_session
def upload():
""" Save a file from AJAX request
Files are saved in UPLOADED_FILES_DEST (see config.local.py) """
err_code = 'TETA_ERR'
RND = int(random.random()*10)
if RND % 2:
# Randomly generate error
print err_code
return err_code
uploaded_files = []
if len(request.files) > 0 and request.files['files']:
uploaded_files = request.files.getlist("files")
print "Uploaded files:"
for f in uploaded_files:
print ' [+] %s [%s]' % (f.filename, f.content_type)
# Before saving you should:
# - Secure the filename
# - Check file size
# - Check content type
f.save(os.path.join(app.config['UPLOADED_FILES_DEST'], f.filename))
f.close()
return "OK"
########################################################################
# Main
########################################################################
if __name__ == '__main__':
app.run(host='0.0.0.0')

48
tetawebapp.sql 普通文件
查看文件

@ -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');