commit 99181ffa08d68590f8599723b052c4be67d74467 Author: Doug Le Tough Date: Mon Feb 26 10:41:52 2018 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4ce8ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*un~ +*.swp +*.pyc +*.wsgi diff --git a/config.local.py b/config.local.py new file mode 100644 index 0000000..49c93c7 --- /dev/null +++ b/config.local.py @@ -0,0 +1,3 @@ +SQLALCHEMY_TRACK_MODIFICATIONS = True +SQLALCHEMY_DATABASE_URI = "postgresql://tetawebapp:tetawebapp@localhost/tetawebapp" +UPLOADED_FILES_DEST = "./upload" diff --git a/config.py b/config.py new file mode 120000 index 0000000..26e8fb0 --- /dev/null +++ b/config.py @@ -0,0 +1 @@ +config.local.py \ No newline at end of file diff --git a/static/fonts/RobotoCondensed-Bold.ttf b/static/fonts/RobotoCondensed-Bold.ttf new file mode 100644 index 0000000..3e06c7c Binary files /dev/null and b/static/fonts/RobotoCondensed-Bold.ttf differ diff --git a/static/fonts/RobotoCondensed-Regular.ttf b/static/fonts/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..b9fc49c Binary files /dev/null and b/static/fonts/RobotoCondensed-Regular.ttf differ diff --git a/static/images/404.png b/static/images/404.png new file mode 100644 index 0000000..2d1f314 Binary files /dev/null and b/static/images/404.png differ diff --git a/static/images/add.png b/static/images/add.png new file mode 100644 index 0000000..0613def Binary files /dev/null and b/static/images/add.png differ diff --git a/static/images/dummy_pic.png b/static/images/dummy_pic.png new file mode 100644 index 0000000..a545628 Binary files /dev/null and b/static/images/dummy_pic.png differ diff --git a/static/images/edit.png b/static/images/edit.png new file mode 100644 index 0000000..f52c063 Binary files /dev/null and b/static/images/edit.png differ diff --git a/static/images/favicon.png b/static/images/favicon.png new file mode 100644 index 0000000..8ccd289 Binary files /dev/null and b/static/images/favicon.png differ diff --git a/static/images/login.png b/static/images/login.png new file mode 100644 index 0000000..b0b3ae7 Binary files /dev/null and b/static/images/login.png differ diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..3ff1932 Binary files /dev/null and b/static/images/logo.png differ diff --git a/static/images/logout.png b/static/images/logout.png new file mode 100644 index 0000000..b3806f4 Binary files /dev/null and b/static/images/logout.png differ diff --git a/static/images/refresh.png b/static/images/refresh.png new file mode 100644 index 0000000..2fae1f2 Binary files /dev/null and b/static/images/refresh.png differ diff --git a/static/images/save.png b/static/images/save.png new file mode 100644 index 0000000..1e981f3 Binary files /dev/null and b/static/images/save.png differ diff --git a/static/images/search.png b/static/images/search.png new file mode 100644 index 0000000..7eb60c5 Binary files /dev/null and b/static/images/search.png differ diff --git a/static/images/trash.png b/static/images/trash.png new file mode 100644 index 0000000..5677370 Binary files /dev/null and b/static/images/trash.png differ diff --git a/static/images/upload.png b/static/images/upload.png new file mode 100644 index 0000000..0c4969a Binary files /dev/null and b/static/images/upload.png differ diff --git a/static/scripts/tetawebapp.js b/static/scripts/tetawebapp.js new file mode 100644 index 0000000..288e6bd --- /dev/null +++ b/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 / + // 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 to the specified + // if 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); +} diff --git a/static/styles/colors.css b/static/styles/colors.css new file mode 100644 index 0000000..59daf9d --- /dev/null +++ b/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); +} diff --git a/static/styles/fonts.css b/static/styles/fonts.css new file mode 100644 index 0000000..425ce76 --- /dev/null +++ b/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); +} diff --git a/static/styles/tetawebapp.css b/static/styles/tetawebapp.css new file mode 100644 index 0000000..2f57c9b --- /dev/null +++ b/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; +} diff --git a/templates/ajax.html b/templates/ajax.html new file mode 100644 index 0000000..99d9fc1 --- /dev/null +++ b/templates/ajax.html @@ -0,0 +1,49 @@ +{% extends "index.html" %} +{% block title %}Ajax{% endblock %} + {% block main %} +
+
+

Get HTML response from AJAX

+

Click the refresh button to get the HTML response.

+

The response may randomly be a voluntary error so you should try it more than once.

+ Refresh: +
+
+

Upload files with AJAX

+

Select files to upload

+

The response may randomly be a voluntary error so you should try it more than once.

+ Upload files: +
+ + + +
+
+
+
+
+
+
+
+

Set value via AJAX

+

Send value to the application.

+

If value is empty or is "We Make Porn" (case sensitive), an error is raised.

+ + +
+
+

Get value from AJAX

+

Get a random value from the application.

+

Randomly raises a voluntary error so you should try it more than once.

+ + +
+
+ {% endblock %} diff --git a/templates/ajax_html.html b/templates/ajax_html.html new file mode 100644 index 0000000..2829abc --- /dev/null +++ b/templates/ajax_html.html @@ -0,0 +1,24 @@ +

This is the title

+dummy pic +

+ 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 +

+

This link will lead to an error page

+

+ 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. +

+

+ 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. +

diff --git a/templates/articles.html b/templates/articles.html new file mode 100644 index 0000000..4c2f6c6 --- /dev/null +++ b/templates/articles.html @@ -0,0 +1,12 @@ +{% extends "index.html" %} +{% block title %}Articles{% endblock %} + {% block main %} +
+

Choose your article

+

+ Please select your article +

+
+
+
+ {% endblock %} diff --git a/templates/articles_by_id.html b/templates/articles_by_id.html new file mode 100644 index 0000000..b17b68c --- /dev/null +++ b/templates/articles_by_id.html @@ -0,0 +1,65 @@ +{% extends "index.html" %} +{% block title %}Articles{% endblock %} + {% block main %} +
+

Article #{{ ID }}

+ dummy pic +

+ 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 +

+

This link will lead to an error page

+

+ 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. +

+
    +
  • plop
  • +
  • plap
  • +
  • plip
  • +
+
    +
  1. plop
  2. +
  3. plap
  4. +
  5. plip
  6. +
+

+ 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. +

+
+
+

Another disposition

+ dummy pic +

+ 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 +

+

+ 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 + 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. +

+
+ {% endblock %} diff --git a/templates/basics.html b/templates/basics.html new file mode 100644 index 0000000..941399f --- /dev/null +++ b/templates/basics.html @@ -0,0 +1,68 @@ +{% extends "index.html" %} +{% block title %}Basics{% endblock %} + {% block main %} +
+

Basics

+

+ Thanks to Python/Flask with TetaWebApp most of the output things come to life via + Jinja2 HTML templates + and is 100% HTML5 ready©. +

+

+ Colors and fonts are managed from separated CSS files letting you easily + change the default theme to your favorite colors and icon set. +

+
+/*
+* 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);
+}
+        
+
+/*
+* 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);
+}
+        
+
+ {% endblock %} diff --git a/templates/database.html b/templates/database.html new file mode 100644 index 0000000..df73b8a --- /dev/null +++ b/templates/database.html @@ -0,0 +1,12 @@ +{% extends "index.html" %} +{% block title %}Database{% endblock %} + {% block main %} +
+

Accessing database

+

+ Even if using Flask-SQLAlchemy to retrieve data + stored in Postgres databases is the recommended way to use TetaWebApp, + you're free to use the database connector that suits your need. +

+
+ {% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..b04587e --- /dev/null +++ b/templates/error.html @@ -0,0 +1,15 @@ +{% extends "index.html" %} +{% block title %}Erreur{% endblock %} +{% block nav %}{% endblock %} +{% block main %} +
+

404 - Not found

+

The page you asked for was not found on this server.
+ It may has been lost, it may has never existed.
+ Maybe we don't care at all...

+

+ +

+ 404 - Not found +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ece493a --- /dev/null +++ b/templates/index.html @@ -0,0 +1,106 @@ + + + + TetaWebApp - {% block title %}Accueil{% endblock %} + + + + + + + + +{% block bodyheader %} + +{% endblock %} +
{% block banner %}TetaWebApp{% endblock %}
+
+ {% block nav %} + + {% endblock%} +
+ {% if navbar %} + + {% endif %} + {% block main %} +
+

TetaWebApp demo

+

+ Welcome to the TetaWebApp demo +

+

+ TetaWebApp is a basic web application template based on Python/Flask + and AJAX made by + Doug Le Tough from Tetalab. +

+

+ 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 without using any Google, + Bootstrap or any other piece of shitty free spyware. +

+ TetaWebApp will never download or upload anything in any way. +

+

+

There is no limitation, you can use all or only parts of TetaWebApp + and you can virtually do any app you want with TetaWebApp. +

+

+ But be sure that freedom has a cost: You will need work to make it work ;-) +

+

+ TetaWebApp is released under the only real free license: The + WTFPL. +

+
+        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
+                    Version 2, December 2004 
+
+ Copyright (C) 2004 Sam Hocevar  
+
+ 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.
+        
+

+ Get a copy of TetaWebApp:
+

+git clone git://git.tetalab.org/tetalab/tetawebapp
+          
+

+
+ {% endblock %} +
+
+ {% block footer %} +
© - Tetalab - Le hacker space Toulousaing' putaing' cong' -
+ {% endblock%} + + diff --git a/templates/inputs.html b/templates/inputs.html new file mode 100644 index 0000000..9059bc8 --- /dev/null +++ b/templates/inputs.html @@ -0,0 +1,53 @@ +{% extends "index.html" %} +{% block title %}Inputs{% endblock %} + {% block main %} +
+

The input collection

+

+ Have a look to the input collection: +

+ + +
+ +
+ + +
+ +
+ + + + + + + + + +
+ + +
+
+
+#!/bin/sh
+# This is code sample
+while [ 1 ]
+do
+  echo "Tits or GTFO !"
+  sleep .1
+done
+        
+
+ {% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..00406f9 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,23 @@ +{% extends "index.html" %} +{% block title %}Login{% endblock %} +{% block nav %}{% endblock %} +{% block main %} + + {% if message != '' %} +
{{ message }}
+ {% endif %} +
+
+ Login: + Password: + +
+
+{% endblock %} diff --git a/templates/todo.html b/templates/todo.html new file mode 100644 index 0000000..9d576c8 --- /dev/null +++ b/templates/todo.html @@ -0,0 +1,18 @@ +{% extends "index.html" %} +{% block title %}TODO{% endblock %} + {% block main %} +
+

TODO list

+
    +
  • Basic menu management
  • +
  • Installation wizard
  • +
  • Back office for basic content management
  • +
  • Basic Ajax support
  • +
  • Session management
  • +
  • File upload
  • +
  • Basic documentation
  • +
  • Horizontal navbar
  • +
  • License
  • +
+
+ {% endblock %} diff --git a/tetawebapp.py b/tetawebapp.py new file mode 100755 index 0000000..6f97cbc --- /dev/null +++ b/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/']}, 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/']}, 0, 0], + [u'Second article', {u'/articles/2': [u'/articles', u'/articles/']}, 0, 0], + [u'Third article', {u'/articles/3': [u'/articles', u'/articles/']}, 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/", 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) + 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/", 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') diff --git a/tetawebapp.sql b/tetawebapp.sql new file mode 100644 index 0000000..72b84dc --- /dev/null +++ b/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');