diff --git a/.travis.yml b/.travis.yml
index 4944b5857..616351860 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,9 +4,11 @@ cache: pip
language: python
python:
- - "3.6"
- "3.7"
- "3.8"
+ - "3.9"
+ - "3.10"
+ - "3.11"
script:
- make test
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 14ab7f41c..d813ccdc7 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,21 +1,24 @@
{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
-
{
"name": "Python: py4web",
"type": "python",
"request": "launch",
"program": "py4web.py",
"args": [
- "run",
+ "run", "--errorlog=:stdout", "-L", "20",
"apps"
],
"console": "integratedTerminal",
- "justMyCode": true,
+ "justMyCode": true
+ },
+ {
+ "name": "Python: File",
+ "type": "python",
+ "request": "launch",
+ "program": "${file}",
+ "justMyCode": true
}
]
-}
\ No newline at end of file
+}
diff --git a/Makefile b/Makefile
index d54dd8e42..753b97a86 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: clean docs clean-assets assets test setup run build deploy
+.PHONY: clean docs clean-assets assets tests setup run build deploy
asset-apps := _dashboard _default _scaffold _minimal _documentation showcase
asset-zips := $(asset-apps:%=py4web/assets/py4web.app.%.zip)
clean:
@@ -6,7 +6,7 @@ clean:
find . -name '*~' -delete
find . -name '#*' -delete
rm -rf dist/*
-clean-assets:
+clean-assets: clean
rm -f py4web/assets/*
mkdir -p py4web/assets
assets: clean-assets $(asset-zips)
@@ -14,29 +14,27 @@ py4web/assets/py4web.app.%.zip: apps/%
cd $< && find . | \
egrep "\.(py|html|css|js|png|jpg|gif|json|yaml|md|txt|mm|ico)$$" | \
zip -@ $(addprefix ../../, $@)
-venv:
- python3 -m venv venv
- venv/bin/pip install -U pip
- venv/bin/pip install ./
-docs: venv
- venv/bin/pip install -U -r docs/requirements.txt
- cd docs; . ../venv/bin/activate && ./updateDocs.sh html
-test: venv
- venv/bin/pip install -U -r test-requirements.txt
- venv/bin/python -m pytest --cov=py4web --cov-report html:cov.html -v -s tests/
+docs:
+ pip install -U -r docs/requirements.txt
+ cd docs; ./updateDocs.sh html
+tests:
+ pip install -U -r test-requirements.txt
+ python -m pytest --cov=py4web --cov-report html:cov.html -v tests/
setup:
- venv/bin/python py4web.py setup apps
- venv/bin/python py4web.py set_password
+ python py4web.py setup apps
+ python py4web.py set_password
run:
- venv/bin/python py4web.py run -p password.txt apps
+ python py4web.py run -p password.txt apps -L20
upgrade-utils:
find apps -name "utils.js" -exec cp apps/_dashboard/static/js/utils.js {} \;
upgrade-vue:
curl -L https://unpkg.com/vue/dist/vue.min.js > apps/_dashboard/static/js/vue.min.js
find apps -name "vue.min.js" -exec cp apps/_dashboard/static/js/vue.min.js {} \;
build: clean assets
- python3 -m pip install --upgrade build
- python3 -m pip install --upgrade twine
- python3 -m build
+ pip install --upgrade build
+ pip install --upgrade twine
+ python -m build
deploy: build
- python3 -m twine upload dist/*
+ python -m twine upload dist/*
+install:
+ python -m pip install .
diff --git a/README.rst b/README.rst
index cea7c9c85..eb6d2c214 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,6 @@
What is py4web?
===============
-.. image:: https://travis-ci.com/web2py/py4web.svg?branch=master
- :target: https://travis-ci.com/web2py/py4web
-
.. image:: https://img.shields.io/pypi/v/py4web.svg
:target: https://pypi.org/project/py4web/
@@ -22,32 +19,32 @@ Screenshots
Running py4web
-.. image:: docs/images/first_run.png
+.. image:: https://py4web.com/_documentation/static/en/_images/first_run.png
The main Dashboard
-.. image:: docs/images/dashboard_main.png
+.. image:: https://py4web.com/_documentation/static/en/_images/dashboard_main.png
Editing a file in the Dashboard
-.. image:: docs/images/dashboard_edit.png
+.. image:: https://py4web.com/_documentation/static/en/_images/dashboard_edit.png
Editing a database in the Dashboard
-.. image:: docs/images/dashboard_restapi.png
+.. image:: https://py4web.com/_documentation/static/en/_images/dashboard_restapi.png
Installation
############
-PY4WEB runs fine on Windows, MacOS and Linux. There are many installation procedures (see the official documentation for details) but only two of them are summarized here.
+PY4WEB runs fine on Windows, MacOS and Linux. There are many installation procedures `(see the official documentation for details) `__ but only two of them are summarized here.
The **simplest way** to install py4web is using binaries, but it's only available for Windows and MacOS. It's meant especially for newbies or students, because it does not require Python pre-installed on your system nor administrative rights. You just need to download the latest Windows or MacOS ZIP file from `this external repository `__. Unzip it on a local folder and open a command line there. Finally run the commands (omit './' if you're using Windows)
.. code:: bash
- ./py4web-start set_password
- ./py4web-start run apps
+ ./py4web set_password
+ ./py4web run apps
@@ -78,35 +75,42 @@ Launch Arguments
# py4web run -h
- Usage: py4web.py run [OPTIONS] [APPS_FOLDER]
+ Usage: py4web.py run [OPTIONS] APPS_FOLDER
Run all the applications on apps_folder
Options:
- -Y, --yes No prompt, assume yes to questions [default:
- False]
- -H, --host TEXT Host name [default: 127.0.0.1]
- -P, --port INTEGER Port number [default: 8000]
- -p, --password_file TEXT File for the encrypted password [default:
- password.txt]
- -s, --server [default|wsgiref|tornado|gunicorn|gevent|waitress|
- geventWebSocketServer|wsgirefThreadingServer|rocketServer]
- server to use [default: default]
- -w, --number_workers INTEGER Number of workers [default: 0]
- -d, --dashboard_mode TEXT Dashboard mode: demo, readonly, full,
- none [default: full]
- --watch [off|sync|lazy] Watch python changes and reload apps
- automatically, modes: off, sync, lazy
- [default: lazy]
- --ssl_cert PATH SSL certificate file for HTTPS
- --ssl_key PATH SSL key file for HTTPS
- --errorlog TEXT Where to send error logs
- (:stdout|:stderr|tickets_only|{filename})
- [default: :stderr]
- -L, --logging_level INTEGER The log level (0 - 50) [default: 30
- (=WARNING)]
- -D, --debug Debug switch [default: False]
- -help, -h, --help Show this message and exit.
+ -Y, --yes No prompt, assume yes to questions
+ -H, --host TEXT Host listening IP [default: 127.0.0.1]
+ -P, --port INTEGER Port number [default: 8000]
+ -A, --app_names TEXT List of apps to run, comma separated (all if
+ omitted or empty)
+ -p, --password_file TEXT File for the encrypted password [default:
+ password.txt]
+ -Q, --quiet Suppress server output
+ -R, --routes Write apps routes to file
+ -s, --server [default|wsgiref|tornado|gunicorn|gevent|waitress|gunicorn|gunicornGevent|
+ gevent|geventWebSocketServer|geventWs|
+ wsgirefThreadingServer|wsgiTh|rocketServer]
+ Web server to use
+ -w, --number_workers INTEGER Number of workers [default: 0]
+ -d, --dashboard_mode TEXT Dashboard mode: demo, readonly, full, none
+ [default: full]
+ --watch [off|sync|lazy] Watch python changes and reload apps
+ automatically, modes: off, sync, lazy
+ [default: lazy]
+ --ssl_cert PATH SSL certificate file for HTTPS
+ --ssl_key PATH SSL key file for HTTPS
+ --errorlog TEXT Where to send error logs
+ (:stdout|:stderr|tickets_only|{filename})
+ [default: :stderr]
+ -L, --logging_level INTEGER The log level (0 - 50) [default: 30
+ (=WARNING)]
+ -D, --debug Debug switch
+ -U, --url_prefix TEXT Prefix to add to all URLs in and out
+ -m, --mode TEXT default or development [default: default]
+ -h, -help, --help Show this message and exit.
+
@@ -180,4 +184,4 @@ Many thanks to everyone who has contributed to the project, and especially:
- `sugizo `__
- `valq7711 `__
- `Kevin Keller `__
-- `Sam de Alfaro `__ (logo design)
+- Sam de Alfaro sam@dealfaro.com (logo design)
diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py
index 47168e057..fcab85dce 100644
--- a/apps/_dashboard/__init__.py
+++ b/apps/_dashboard/__init__.py
@@ -194,7 +194,7 @@ def apps():
apps.sort(key=lambda item: item["name"])
return {"payload": apps, "status": "success"}
- @action("delete_app/", method="POST")
+ @action("delete_app/", method="POST")
@session_secured
def delete_app(name):
"""delete the app"""
@@ -209,7 +209,7 @@ def delete_app(name):
return {"status": "success", "payload": "Deleted"}
return {"status": "success", "payload": "App does not exist"}
- @action("new_file//", method="POST")
+ @action("new_file//", method="POST")
@session_secured
def new_file(name, file_name):
"""creates a new file"""
@@ -417,9 +417,9 @@ def save(path, reload_app=True):
"""Saves a file"""
app_name = path.split("/")[0]
path = safe_join(FOLDER, path) or abort()
- with open(path, "w") as myfile:
+ with open(path, "wb") as myfile:
body = json.load(request.body)
- myfile.write(body)
+ myfile.write(body.encode("utf8"))
if reload_app:
Reloader.import_app(app_name)
return {"status": "success"}
@@ -567,7 +567,10 @@ def gitshow(project, commit):
@action.uses(Logged(session), "translations.html")
def translations(name):
"""returns a json with all translations for all languages"""
- t = Translator(os.path.join(FOLDER, name, "translations"))
+ folder = os.path.join(FOLDER, name, "translations")
+ if not os.path.exists(folder):
+ os.makedirs(folder)
+ t = Translator(folder)
return t.languages
diff --git a/apps/_dashboard/static/components/mtable.js b/apps/_dashboard/static/components/mtable.js
index 413c03a87..7a42bb508 100644
--- a/apps/_dashboard/static/components/mtable.js
+++ b/apps/_dashboard/static/components/mtable.js
@@ -52,10 +52,10 @@
if (filters.length) url += '&'+filters.join('&');
if (self.order) url += '&@order='+self.order;
self.busy = true;
- axios.get(url).then(function (res) {
+ Q.get(url).then(function (res) {
self.busy = false;
- if(!length) self.table = res.data;
- else self.table.items = self.table.items.concat(res.data.items);
+ if(!length) self.table = res.json();
+ else self.table.items = self.table.items.concat(res.json().items);
});
};
@@ -100,8 +100,8 @@
reference_table_url.pop()
reference_table_url.push(field.references)
reference_table_url = reference_table_url.join('/') + '?@options_list=true';
- axios.get(reference_table_url).then(function (res) {
- let url_components = res.config.url.split('?')[0].split('/');
+ Q.get(reference_table_url).then(function (res) {
+ let url_components = res.json().config.url.split('?')[0].split('/');
self.reference_options[url_components[url_components.length - 1 ]] = res.data.items;
});
@@ -129,7 +129,7 @@
if (window.confirm("Really delete record?")) {
let url = this.url + '/' + item.id;
this.table.items = this.table.items.filter((i)=>{return i.id != item.id;});
- axios.delete(url);
+ Q.delete(url);
if (item==this.item) this.item = null;
}
};
@@ -150,27 +150,30 @@
}
if (item.id) {
url += '/' + item.id;
- axios.put(url, item).then(mtable.handle_response('put', this),
- mtable.handle_response('put', this));
+ var data = JSON.parse(JSON.stringify(item));
+ delete data["id"];
+ Q.put(url, data).then(mtable.handle_response('put', this),
+ mtable.handle_response('put', this));
} else {
- axios.post(url, item).then(mtable.handle_response('post', this),
- mtable.handle_response('post', this));
- }
+ Q.post(url, item).then(mtable.handle_response('post', this),
+ mtable.handle_response('post', this));
+ }
};
mtable.handle_response = function(method, data) {
self.busy = false;
return function(res) {
- if (res.response) res = res.response; // deal with error weirdness
+ res = res.json();
if (method == 'post') {
data.table.items = [];
mtable.methods.load.call(data);
}
- if (res.data.status == 'success') {
+ if (res.status == 'success') {
data.clear();
+ location.reload();
} else {
- data.errors = res.data.errors;
- data.message = res.data.message;
+ data.errors = res.errors;
+ data.message = res.message;
}
};
};
diff --git a/apps/_dashboard/static/js/axios.min.js b/apps/_dashboard/static/js/axios.min.js
deleted file mode 100644
index 2d030546a..000000000
--- a/apps/_dashboard/static/js/axios.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* axios v0.20.0 | (c) 2020 by Matt Zabriskie */
-!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(4),a=n(22),u=n(10),c=r(u);c.Axios=s,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function i(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function s(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){u.headers[e]={}}),i.forEach(["post","put","patch"],function(e){u.headers[e]=i.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),i=n(16),s=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"],(r.isBlob(p)||r.isFile(p))&&p.type&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=unescape(encodeURIComponent(e.auth.password))||"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),s(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,i=e.responseType&&"text"!==e.responseType?l.response:l.responseText,s={data:i,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,s),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?i.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(i)&&a.push("domain="+i),s===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(i[o]=n(void 0,e[o])):i[o]=n(e[o],t[o])}t=t||{};var i={},s=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(s,function(e){r.isUndefined(t[e])||(i[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(i[o]=n(void 0,e[o])):i[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?i[r]=n(e[r],t[r]):r in e&&(i[r]=n(void 0,e[r]))});var f=s.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),i}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])});
-//# sourceMappingURL=axios.min.map
\ No newline at end of file
diff --git a/apps/_dashboard/static/js/index.js b/apps/_dashboard/static/js/index.js
index 26072e340..9b0d1b515 100644
--- a/apps/_dashboard/static/js/index.js
+++ b/apps/_dashboard/static/js/index.js
@@ -9,16 +9,6 @@ Vue.component('treefiles', {
}
});
-let post= function(url, data) {
- return fetch(url, {
- method: "POST",
- cache: "no-cache",
- headers: { "Content-Type": "application/json" },
- redirect: "follow",
- body: JSON.stringify(data)
- });
-};
-
let app = {};
let init = (app) => {
app.data = {
@@ -43,9 +33,7 @@ let init = (app) => {
};
app.select_app = (appobj) => {
app.vue.selected_app = appobj;
- app.vue.walk = [];
- fetch('../walk/'+appobj.name).then(r=>r.json()).then(r=>{app.vue.walk=r.payload;});
- fetch('../rest/'+appobj.name).then(r=>r.json()).then(r=>{app.vue.databases=r.databases;});
+ app.reload_files();
};
app.activate_editor = (path, payload) => {
app.vue.files[path] = payload;
@@ -80,8 +68,8 @@ let init = (app) => {
} else {
var url = '../load/'+path;
if(app.vue.selected_type != 'text') url = '../load_bytes/'+path;
- fetch(url).then(r=>r.json()).then(r=>{
- app.activate_editor(path, r.payload);
+ Q.get(url).then(r=>{
+ app.activate_editor(path, r.json().payload);
});
}
}
@@ -93,7 +81,7 @@ let init = (app) => {
app.reload = (name) => {
app.modal_dismiss();
app.vue.loading = true;
- fetch(name?'../reload/'+name:'../reload').then(r=>r.json()).then(r=>app.init());
+ Q.get(name?'../reload/'+name:'../reload').then(r=>app.init());
};
app.gitlog = (name) => {
window.open('../gitlog/'+name);
@@ -106,9 +94,13 @@ let init = (app) => {
// pass
};
app.save_file = () => {
+ if(app.vue.selected_type != 'text') {
+ alert("Unable to save this file, it is not of type text");
+ return;
+ }
var path = app.vue.selected_filename;
app.vue.files[path] = app.editor.getValue();
- post('../save/'+path, app.vue.files[path]).then(r=>app.file_saved());
+ Q.post('../save/'+path, app.vue.files[path]).then(r=>app.file_saved());
};
app.download_selected_app = () => {
var url = '../packed/py4web.app.' + app.vue.selected_app.name + '.zip?' + (new Date()).getTime()
@@ -129,7 +121,7 @@ let init = (app) => {
else if(form.mode=='new' && app.vue.apps.map((a)=>{return a.name;}).indexOf(form.name)>=0) {
alert('Cannot create an app with this name. It already exists');
} else {
- post('../new_app', form).then(r=>app.reload());
+ Q.post('../new_app', form).then(r=>app.reload());
}
};
app.process_new_file = () => {
@@ -137,9 +129,9 @@ let init = (app) => {
var form = app.vue.modal.form;
if(!form.filename) { alert('An file name must be provided'); return; }
/*reload entire page needed to see the new file listed*/
- post('../new_file/'+app_name+'/'+form.filename).then(r=>{
+ Q.post('../new_file/'+app_name+'/'+form.filename).then(r=>{
app.vue.walk = [];
- fetch('../walk/'+app_name).then(r=>r.json()).then(r=>{app.vue.walk=r.payload;});
+ Q.get('../walk/'+app_name).then(r=>{app.vue.walk=r.json().payload;});
app.modal_dismiss();
});
};
@@ -176,54 +168,63 @@ let init = (app) => {
var name = app.vue.selected_filename;
app.confirm("Delete File","blue","Do you really want to delete "+name+"?",()=>{
app.modal_dismiss();
- post('../delete/'+name).then(r=>app.init());
+ Q.post('../delete/'+name).then(r=>app.init());
});
};
app.delete_selected_app = () => {
var name = app.vue.selected_app.name;
app.confirm("Delete App","blue","Do you really want to delete "+name+"?",()=>{
app.modal_dismiss();
- post('../delete_app/'+name).then(r=>app.init());
+ Q.post('../delete_app/'+name).then(r=>app.init());
});
};
app.reload_info = () => {
- fetch('../info').then(r=>r.json()).then(r=>{
- app.vue.info=r.payload || [];
+ Q.get('../info').then(r=>{
+ app.vue.info=r.json().payload || [];
});
};
app.reload_apps = () => {
- fetch('../apps').then(r=>r.json()).then(r=>{
- app.vue.apps=r.payload || []; app.update_selected();
+ Q.get('../apps').then(r=>{
+ app.vue.apps=r.json().payload || []; app.update_selected();
});
};
app.reload_routes = () => {
- fetch('../routes').then(r=>r.json()).then(r=>{
- app.vue.routes=r.payload || [];
+ Q.get('../routes').then(r=>{
+ app.vue.routes=r.json().payload || [];
});
};
app.reload_tickets = () => {
app.vue.tickets = [];
- fetch('../tickets').then(r=>r.json()).then(r=>{
- app.vue.tickets = r.payload || [];
+ Q.get('../tickets').then(r=>{
+ app.vue.tickets = r.json().payload || [];
});
};
+ app.reload_files = () => {
+ if (!app.vue.selected_app) return;
+ app.vue.walk = [];
+ var name = app.vue.selected_app.name;
+ Q.get('../walk/'+name).then(r=>{app.vue.walk=r.json().payload;});
+ Q.get('../rest/'+name).then(r=>{app.vue.databases=r.json().databases;});
+ app.vue.selected_filename = null;
+ }
app.clear_tickets = () => {
app.vue.tickets = [];
- fetch('../clear').then(r=>app.reload_tickets());
+ Q.get('../clear').then(r=>app.reload_tickets());
};
app.login = () => {
- post('../login', {'password': app.vue.password})
- .then(r=>r.json())
+ Q.post('../login', {'password': app.vue.password})
+
.then(r=>{
app.vue.password = '';
if( r.user) {
app.vue.user = true;
app.init();
}
+ location.reload();
});
};
app.logout = () => {
- post('../logout').then(r=>window.location.reload());
+ Q.post('../logout').then(r=>window.location.reload());
};
app.methods = {
select: app.select_app,
@@ -258,11 +259,12 @@ let init = (app) => {
return a.name==app.vue.selected_app.name;
})[0];
};
- app.init = () => {
+ app.init = () => {
app.reload_info();
app.reload_apps();
app.reload_routes();
app.reload_tickets();
+ app.reload_files();
setTimeout(()=>{app.vue.loading=false;}, 1000);
};
if (USER_ID) app.init();
diff --git a/apps/_dashboard/static/js/translations.js b/apps/_dashboard/static/js/translations.js
index fcd134272..50f743443 100644
--- a/apps/_dashboard/static/js/translations.js
+++ b/apps/_dashboard/static/js/translations.js
@@ -33,15 +33,15 @@ var init_app = function() {
};
self.methods.save_languages = function() {
let data = self.vue.translations;
- axios.post('/' + self.base + '/api/translations/' + self.app, data).then(
+ Q.post('/' + self.base + '/api/translations/' + self.app, data).then(
function(res) { alert("Saved"); },
function(res) { alert("Error Saving"); }
);
};
self.methods.update_languages = function() {
- axios.get('/' + self.base + '/api/translations/' + self.app + '/search').then(
+ Q.get('/' + self.base + '/api/translations/' + self.app + '/search').then(
function(res) {
- let words = res.data.strings;
+ let words = res.json().strings;
for (var lang in self.vue.translations) {
var translations = self.vue.translations[lang];
words.map(function(key) {
@@ -78,8 +78,8 @@ var init_app = function() {
self.methods.select_language(language);
};
self.vue = new Vue({ el: '#vue', data: self.data, methods: self.methods });
- axios.get('/' + self.base + '/api/translations/' + self.app).then(
- function(res) { self.vue.translations = res.data; }
+ Q.get('/' + self.base + '/api/translations/' + self.app).then(
+ function(res) { self.vue.translations = res.json(); }
);
return self;
}
diff --git a/apps/_dashboard/static/js/utils.js b/apps/_dashboard/static/js/utils.js
index c0ba784d2..aa2bd424c 100644
--- a/apps/_dashboard/static/js/utils.js
+++ b/apps/_dashboard/static/js/utils.js
@@ -43,12 +43,18 @@ Q.ajax = function(method, url, data, headers) {
return new Promise(function(resolve, reject) {
fetch(url, options).then(function(res){
res.text().then(function(body){
- res.data = body;
+ res.data = body;
res.json = function(){return JSON.parse(body);};
resolve(res);
}, reject);}).catch(reject);
});
}
+
+Q.get = (url, headers) => Q.ajax("GET", url, null, headers);
+Q.post = (url, data, headers) => Q.ajax("POST", url, data, headers);
+Q.put = (url, data, headers) => Q.ajax("PUT", url, data, headers);
+Q.delete = (url, headers) => Q.ajax("DELETE", url, null, headers);
+
// Gets a cookie value
Q.get_cookie = function (name) {
var cookie = RegExp("" + name + "[^;]+").exec(document.cookie);
diff --git a/apps/_dashboard/templates/dbadmin.html b/apps/_dashboard/templates/dbadmin.html
index 7ab18ec7f..e501ec664 100644
--- a/apps/_dashboard/templates/dbadmin.html
+++ b/apps/_dashboard/templates/dbadmin.html
@@ -21,7 +21,6 @@
+
+
+
+ WHAT IS PY4WEB?
+
+
py4web is a framework for rapid development of secure database driven web applications.
+ It is the successor of
web2py but much improved.
+
+
+ Install it and start it
+
+
+$ pip install py4web # install it (but use a venv or Nix)
+$ py4web setup apps # answer yes to all questions
+$ py4web set_password # pick a password for the admin
+$ cp -r apps/_scaffold apps/myapp # make a new app
+$ py4web run apps # start py4web
+
+
+Each subfolder of
apps/ with an
__init__.py is its own app. One py4web can run multiple apps.
+You just copy the
_scaffold app to make a new one.
+
+
+
+ The basic functions/objects are imported from the py4web module.
+
+
+from py4web import action, redirect, request, URL, Field
+
+
+
+ Use @action to map URLs into functions (aka actions). Actions can return strings or dictionaries.
+
+
+# http://127.0.0.1:8000/myapp/index
+@action("index")
+def index():
+ return "hello world"
+
+
+
+ Actions can map path_info items into variables
+
+
+# http://127.0.0.1:8000/myapp/index/1
+@action("index/<x:int>")
+def index(x):
+ return f"x = {x}"
+
+
+
+ py4web uses a request object from
ombott, compatible with
+
bottlepy
+
+
+# http://127.0.0.1:8000/myapp/index/?x=1
+@action("index")
+def index():
+ x = request.query.get("x")
+ return f"x = {x}"
+
+
+
+ It can parse JSON from POST requests for example
+
+
+# http://127.0.0.1:8000/myapp/index POST {x: 1}
+@action("index", method="POST")
+def index():
+ x = request.json.get("x")
+ return {"x": x}
+
+
+
+ A page can redirect to another page
+
+
+@action("index")
+def index():
+ redirect("http://example.com")
+
+
+
+ We use
URL to generate the urls of internal pages
+
+
+@action("index")
+def index():
+ redirect(URL("other_page"))
+
+
+
+ We have a built-in session object which by default stores the session data, signed, in a cookie. Optionally it can be stored in db, redis,
+ or other custom storage. Session is a
fixture
+ and it must be declared with
@action.uses.
+ Think of fixtures as per action (as opposed to per app) middleware.
+
+
+@action("index")
+@action.uses(session)
+def index():
+ session.x = (session.x or 0) + 1
+ return f"x = {x}"
+
+
+
+ An action can return a dictionary and use a
template to render the dictionary into HTML. A template is also a fixture and it must be declared with @action.uses.
+
+
+@action("index")
+@action.uses("index.html")
+def index():
+ x = 1
+ return locals()
+
+
+
+ A template can be any text but typically it is HTML. Templates can extend and include other templates. Templetes can embed variables with
[[=x]] and they can also embed python code (without limitations) with double square brakets. Indentation does not matter.
[[pass]] closes
[[ if ... ]] and
[[ for ... ]].
+
+
+[[extend "layout.html"]]
+x = [[=x]]
+
+[[ for i in range(10): ]][[ if i % 2==0: ]]
+[[=i]] is even
+[[ pass ]][[ pass ]]
+
+
+
+ Py4web comes with a built-in
auth object that generates all the pages
+ required for user registration, login, email verification, retrieve and change password, edit profile, single sign on with OAuth2 and more.
+
auth is also a fixture which exposed the current user to the action. Notice that fixtures have dependencies, and by including
+
auth its dependencies (db, session, flash) are also included automatically.
+
+
+@action("index")
+@action.uses("generic.html", auth)
+def index():
+ user = auth.get_user()
+ if user:
+ message = f"Hello {user['first_name']}"
+ else:
+ message = "Hello, you are not logged in"
+ return {"message": message}
+
+
+
+
auth.user is a different fixture which requires a logged-in user and blocks access otherwise
+
+
+@action("index")
+@action.uses("generic.html", auth.user)
+def index():
+ user = auth.get_user()
+ message = f"Hello {user['first_name']}"
+ return {"message": message}
+
+
+
+ More complex policies are possible using the built-in
tagging
+ system combined with
auth.
+
Condition is another fixture, if False it raises a 404 error page by default.
+
+
+is_manager = Condition(lambda: "manager" in groups.get(auth.user_id))
+
+@action("index")
+@action.uses("generic.html", auth.user, is_manager)
+def index():
+ user = auth.get_user()
+ message = f"Hello {user['first_name']} (manager!)"
+ return {"message": message}
+
+
+
+ Py4web has a built-in Database Abstraction Layer (support for sqlite, postgres, mysql, oracle, and more).
+ It is integrated with
auth and with form generation logic. It follows a declarative pattern and
+ provides automatic migrations to create/alter tables. For example the following code creates a "thing" table with a "name" field and and an "image" and an additional standard signature fields ("created_by", "created_on", "modified_by", "modified_on"). Field types are more complex than basic database types as they have logic for validation and for handling content (such as uploading and downloading images).
+
+
+db.define_table(
+ "thing",
+ Field("name", requires=IS_NOT_EMPTY()),
+ Field("image", "upload", download_url = lambda fn: URL(f"download/{fn}")),
+ auth.signature)
+
+
+
+ Given the object
db.thing defined above py4web can automatically generate
forms including
validation.
+Here is a create form
+
+
+@action("create_thing")
+@action.uses("generic.html", auth.user)
+def create_thing():
+ form = Form(db.thing)
+ if form.accepted:
+ # record created
+ redirect(URL("index"))
+ return locals()
+
+
+
+ Here is an edit form
+
+
+@action("edit_thing/<thing_id:int>")
+@action.uses("generic.html", auth.user)
+def edit_thing(thing_id):
+ form = Form(db.thing, thing_id)
+ if form.accepted:
+ # record updated
+ redirect(URL("index"))
+ return locals()
+
+
+
+ py4web can also generate a
grid from a database query.
+ The grid shows selected records with pagination and, optionally, enables creating, editing, deleting records, with multiple options for customization
+
+
+@action("my_things")
+@action("my_things/<path:path>")
+@action.uses("generic.html", auth.user)
+def my_things(path=None):
+ form = Grid(path,
+ db.thing.created_by==auth.user_id,
+ editable=True, create=True, deletable=True)
+ return locals()
+
+
+
+ The DAL also makes it very easy to create APIs. Here is a GET API example
+
+
+@action("api/things", method="GET")
+@action.uses(db)
+def api_GET_things():
+ return {"things": db(db.thing).select().as_list()}
+
+
+
+ POST API example
+
+
+@action("api/things", method="POST")
+@action.uses(db)
+def api_POST_things():
+ return db.thing.validate_and_insert(**request.json)
+
+
+
+ PUT API example
+
+
+@action("api/things/<thing_id:int>", method="PUT")
+@action.uses(db)
+def api_PUT_things(thing_id):
+ return db.thing.validate_and_update(thing_id, **request.json)
+
+
+
+ DELETE API example
+
+
+@action("api/things/<thing_id:int>", method="DELETE")
+@action.uses(db)
+def api_DELETE_things(thing_id):
+ return {"deleted": db(db.thing.id==thing_id).delete()}
+
+
+
+
+
+
+ These are just the basics. There is a lot more to it, including...
+
+
+
+ LICENSE
+
+ 3-clause BSD
+
+ USEFUL LINKS
+
+
+
+
+
+
+
+
+
+
-
diff --git a/apps/_dashboard/templates/gitlog.html b/apps/_dashboard/templates/gitlog.html
index 9306b2ef5..a2ee5c504 100644
--- a/apps/_dashboard/templates/gitlog.html
+++ b/apps/_dashboard/templates/gitlog.html
@@ -83,8 +83,6 @@
-
-
-
diff --git a/apps/_default/__init__.py b/apps/_default/__init__.py
index b5d43cbb0..a6344d6aa 100644
--- a/apps/_default/__init__.py
+++ b/apps/_default/__init__.py
@@ -1,7 +1,10 @@
-from py4web import action, __version__
-
+import os
+from py4web import action, Cache
+cache = Cache(size=1000)
@action("index")
-@action.uses("index.html")
+@cache.memoize(expiration=1)
def index():
- return dict(version=__version__)
+ filename = os.path.join(os.path.dirname(__file__), "static", "index.html")
+ with open(filename) as stream:
+ return stream.read()
diff --git a/apps/_default/static/css/prism.css b/apps/_default/static/css/prism.css
new file mode 100644
index 000000000..221f20cfd
--- /dev/null
+++ b/apps/_default/static/css/prism.css
@@ -0,0 +1,3 @@
+/* PrismJS 1.29.0
+https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+python&plugins=remove-initial-line-feed+normalize-whitespace */
+code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
diff --git a/apps/_default/static/index.html b/apps/_default/static/index.html
new file mode 100644
index 000000000..b3df0406e
--- /dev/null
+++ b/apps/_default/static/index.html
@@ -0,0 +1,348 @@
+
+