diff --git a/.github/workflows/run_test.yaml b/.github/workflows/run_test.yaml
index e7bd13a3d..008f483eb 100644
--- a/.github/workflows/run_test.yaml
+++ b/.github/workflows/run_test.yaml
@@ -4,6 +4,7 @@ on:
push:
branches:
- master
+ - ombott-dev
pull_request:
branches:
- master
@@ -14,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.7, 3.8, 3.9]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v2
@@ -22,16 +23,13 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Install dependencies
+ - name: Install Everything
run: |
sudo apt-get install memcached libmemcached-tools
python -m pip install --upgrade pip
- python3 -m pip install -r requirements.txt
- python3 -m pip install -r test-requirements.txt
- - name: build
- run: |
- python3 setup.py install
- - name: Test with pytest
+ ls -l
+ python -m pip install -e ./
+ python -m pip install -r test-requirements.txt
+ - name: Test
run: |
python3 -m pytest --cov=py4web --cov-report html:cov.html -v -s tests/
-
diff --git a/.gitignore b/.gitignore
index be3495bf1..12a89d84e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
*.1
*.bak
*.bak2
+.envrc
*.svn
*.w2p
*.class
@@ -22,9 +23,9 @@ Thumbs.db
./*.zip
apps/_documentation/static/*/*.pdf
apps/_documentation/static/*/*.epub
-apps/*
+apps*/*/*
!apps/todo/*
-!apps/examples/*
+!apps/showcase/*
!apps/_dashboard/*
!apps/_scaffold/*
!apps/_minimal/*
@@ -32,7 +33,7 @@ apps/*
!apps/_default/*
!apps/_documentation/*
!apps/superheroes/*
-!apps/myfeed/*
+!apps/fadebook/*
apps/*/databases/*
apps/*/uploads/*
**/*.py[co]
@@ -43,6 +44,11 @@ deployment_tools/gae/lib
deployment_tools/gae/apps
deployment_tools/gae/requirements.txt
py4web/assets
+pyproject.toml
+poetry.lock
workspace.code-workspace
venv
docs/_build
+py4web.egg-info/
+build/
+tmp/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4944b5857..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-dist: bionic
-sudo: required
-cache: pip
-language: python
-
-python:
- - "3.6"
- - "3.7"
- - "3.8"
-
-script:
- - make test
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..d813ccdc7
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,24 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python: py4web",
+ "type": "python",
+ "request": "launch",
+ "program": "py4web.py",
+ "args": [
+ "run", "--errorlog=:stdout", "-L", "20",
+ "apps"
+ ],
+ "console": "integratedTerminal",
+ "justMyCode": true
+ },
+ {
+ "name": "Python: File",
+ "type": "python",
+ "request": "launch",
+ "program": "${file}",
+ "justMyCode": true
+ }
+ ]
+}
diff --git a/Makefile b/Makefile
index 7af45d886..753b97a86 100644
--- a/Makefile
+++ b/Makefile
@@ -1,45 +1,40 @@
-.PHONY: clean build docs clean-assets assets install test deploy
-asset-apps := _dashboard _default _scaffold _minimal _documentation examples
+.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:
find . -name '*.pyc' -delete
find . -name '*~' -delete
find . -name '#*' -delete
rm -rf dist/*
- python3 setup.py clean
-docs:
- cd docs; ./updateDocs.sh all
-clean-assets:
+clean-assets: clean
rm -f py4web/assets/*
mkdir -p py4web/assets
assets: clean-assets $(asset-zips)
py4web/assets/py4web.app.%.zip: apps/%
cd $< && find . | \
- egrep "\.(py|html|css|js|png|jpg|gif|json|yaml|md|txt|mm)$$" | \
+ egrep "\.(py|html|css|js|png|jpg|gif|json|yaml|md|txt|mm|ico)$$" | \
zip -@ $(addprefix ../../, $@)
-build: clean assets
- python3 setup.py build
-install: build
- python3 setup.py install
-test: build
- python3 -m pip install -r requirements.txt
- python3 -m pip install -r test-requirements.txt
- python3 -m pytest --cov=py4web --cov-report html:cov.html -v -s tests/
-push: test
- git push origin master
-deploy: test
- python2.7 setup.py sdist
- twine upload dist/*
+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:
- ./py4web.py setup apps
- ./py4web.py set_password
+ python py4web.py setup apps
+ python py4web.py set_password
run:
- ./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-axios:
- curl -L https://unpkg.com/axios/dist/axios.min.js > apps/_dashboard/static/js/axios.min.js
- find apps -name "axios.min.js" -exec cp apps/_dashboard/static/js/axios.min.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
+ pip install --upgrade build
+ pip install --upgrade twine
+ python -m build
+deploy: build
+ python -m twine upload dist/*
+install:
+ python -m pip install .
diff --git a/apps/examples/static/components-bulma/starrater/starrater.css b/README.md
similarity index 100%
rename from apps/examples/static/components-bulma/starrater/starrater.css
rename to README.md
diff --git a/README.rst b/README.rst
index 32aa56ca8..a133d70ab 100644
--- a/README.rst
+++ b/README.rst
@@ -1,14 +1,13 @@
-What is py4web?
-===============
-
-.. image:: https://travis-ci.com/web2py/py4web.svg?branch=master
- :target: https://travis-ci.com/web2py/py4web
+PY4WEB
+======
.. image:: https://img.shields.io/pypi/v/py4web.svg
:target: https://pypi.org/project/py4web/
-PY4WEB is a web framework for rapid development of efficient database driven web applications. It is an evolution of the popular web2py framework but much faster and slicker.
+.. image:: https://github.com/web2py/py4web/actions/workflows/run_test.yaml/badge.svg
+ :target: https://github.com/web2py/py4web/actions/workflows/run_test.yaml
+PY4WEB is a web framework for the rapid development of efficient database driven web applications. It is an evolution of the popular web2py framework but much faster and slicker.
- Main web site: https://py4web.com
- GitHub repository: https://github.com/web2py/py4web
@@ -16,43 +15,42 @@ PY4WEB is a web framework for rapid development of efficient database driven web
- License: `BSD-3-Clause License `__
-
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
-The **standard installation procedure** for py4web on Windows, MacOS and Linux is using pip. Its only prerequisite is Python 3.6+.
+The **standard installation procedure** for py4web on Windows, MacOS and Linux is using pip. Its only prerequisite is Python 3.7+.
.. code:: bash
@@ -78,29 +76,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
- -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.
+
@@ -124,13 +135,14 @@ Tell me more
############
- it is 10-20x faster than web2py
-- python3.6+ only
+- python3.7+ only
- uses https://github.com/web2py/pydal (same DAL as web2py) for database connection
- uses the same validators as web2py (they are in pyDAL)
- uses `yatl `__ (same as web2py but defaults to [[...]] instead of {{...}} delimiters) and `Renoir `__ for html templates
- uses the very similar html helpers to web2py (A, DIV, SPAN, etc.)
- uses https://github.com/web2py/pluralize for i18n and pluralization
-- request, response, abort are from https://bottlepy.org
+- request, response, abort are from https://bottlepy.org, using `ombott (One More BOTTle) `__,
+ which is a fast bottlepy spin-off
- HTTP and redirect are our own objects
- like web2py, it supports static asset management /{appname}/static/_0.0.0/{path}
- implements sessions in cookies (jwt encrypted), db, memcache, redis and custom
@@ -173,4 +185,5 @@ Many thanks to everyone who has contributed to the project, and especially:
- `sugizo `__
- `valq7711 `__
- `Kevin Keller `__
-- `Sam de Alfaro `__ (logo design)
+- `Krzysztof Socha `__
+- Sam de Alfaro sam@dealfaro.com (logo design)
diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py
index 035b1c54f..fcab85dce 100644
--- a/apps/_dashboard/__init__.py
+++ b/apps/_dashboard/__init__.py
@@ -2,6 +2,7 @@
import copy
import datetime
import io
+import json
import os
import shutil
import subprocess
@@ -10,21 +11,14 @@
import zipfile
import requests
+from pydal.restapi import Policy, RestAPI
from pydal.validators import CRYPT
import py4web
-from py4web import (
- HTTP,
- URL,
- Translator,
- __version__,
- abort,
- action,
- redirect,
- request,
- response,
-)
-from py4web.core import Fixture, Reloader, Session, dumps, error_logger, safely
+from py4web import (HTTP, URL, Translator, __version__, abort, action,
+ redirect, request, response)
+from py4web.core import (DAL, Fixture, Reloader, Session, dumps, error_logger,
+ safely)
from py4web.utils.factories import ActionFactory
from .diff2kryten import diff2kryten
@@ -32,6 +26,7 @@
MODE = os.environ.get("PY4WEB_DASHBOARD_MODE", "none")
FOLDER = os.environ["PY4WEB_APPS_FOLDER"]
+APP_NAMES = os.environ.get("PY4WEB_APP_NAMES")
APP_FOLDER = os.path.dirname(__file__)
T_FOLDER = os.path.join(APP_FOLDER, "translations")
T = Translator(T_FOLDER)
@@ -39,11 +34,29 @@
session = Session()
+def make_safe(db):
+ def make_safe_field(func):
+ def wrapper():
+ try:
+ return func()
+ except Exception as exp:
+ print(exp)
+ print("Warning: _dashboard trying to access a forbidden method of app")
+ return None
+
+ for table in db:
+ for field in table:
+ if callable(field.default):
+ field.default = make_safe_field(field.default)
+ if callable(field.update):
+ field.update = make_safe_field(field.update)
+
+
def run(command, project):
"""for runing git commands inside an app (project)"""
return subprocess.check_output(
command.split(), cwd=os.path.join(FOLDER, project)
- ).decode()
+ ).decode(errors="ignore")
def get_commits(project):
@@ -86,7 +99,7 @@ def __init__(self, session):
self.__prerequisites__ = [session]
self.session = session
- def on_request(self):
+ def on_request(self, context):
user = self.session.get("user")
if not user or not user.get("id"):
abort(403)
@@ -158,24 +171,30 @@ def info():
@session_secured
def routes():
"""Returns current registered routes"""
- return {"payload": Reloader.ROUTES, "status": "success"}
+ sorted_routes = {
+ name: list(sorted(routes, key=lambda route: route["rule"]))
+ for name, routes in Reloader.ROUTES.items()
+ }
+ return {"payload": sorted_routes, "status": "success"}
@action("apps")
@session_secured
def apps():
"""Returns a list of installed apps"""
apps = os.listdir(FOLDER)
+ exposed_names = APP_NAMES and APP_NAMES.split(",")
apps = [
{"name": app, "error": Reloader.ERRORS.get(app)}
for app in apps
if os.path.isdir(os.path.join(FOLDER, app))
and not app.startswith("__")
and not app.startswith(".")
+ and (not exposed_names or app in exposed_names)
]
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"""
@@ -190,10 +209,10 @@ 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):
- """asign an sanitize inputs"""
+ """creates a new file"""
path = os.path.join(FOLDER, name)
form = request.json
if not os.path.exists(path):
@@ -250,7 +269,7 @@ def walk(path):
def load(path):
"""Loads a text file"""
path = safe_join(FOLDER, path) or abort()
- content = open(path, "rb").read().decode("utf8")
+ content = open(path, "rb").read().decode("utf8", errors="ignore")
return {"payload": content, "status": "success"}
@action("load_bytes/")
@@ -265,8 +284,10 @@ def load_bytes(path):
def packed(path):
"""Packs an app"""
appname = path.split(".")[-2]
- appname = sanitize(appname)
+ # some security
app_dir = os.path.join(FOLDER, appname)
+ if "/" in path or appname.startswith(".") or not os.path.exists(app_dir):
+ raise HTTP(400)
store = io.BytesIO()
zip = zipfile.ZipFile(store, mode="w", compression=zipfile.ZIP_DEFLATED)
for root, dirs, files in os.walk(app_dir, topdown=False):
@@ -277,7 +298,6 @@ def packed(path):
):
filename = os.path.join(root, name)
short = filename[len(app_dir + os.path.sep) :]
- print("added", filename, short)
zip.write(filename, short)
zip.close()
data = store.getvalue()
@@ -316,12 +336,12 @@ def api(path):
# this is not final, requires pydal 19.5
args = path.split("/")
app_name = args[0]
- from py4web.core import Reloader, DAL
- from pydal.restapi import RestAPI, Policy
-
if MODE != "full":
raise HTTP(403)
- module = Reloader.MODULES[app_name]
+ module = Reloader.MODULES.get(app_name)
+
+ if not module:
+ raise HTTP(404)
def url(*args):
return request.url + "/" + "/".join(args)
@@ -333,6 +353,7 @@ def url(*args):
def tables(name):
db = getattr(module, name)
+ make_safe(db)
return [
{
"name": t._tablename,
@@ -349,6 +370,7 @@ def tables(name):
}
elif len(args) > 2 and args[1] in databases:
db = getattr(module, args[1])
+ make_safe(db)
id = args[3] if len(args) == 4 else None
policy = Policy()
for table in db:
@@ -365,7 +387,9 @@ def tables(name):
table._tablename, "POST", authorize=True, fields=table.fields
)
policy.set(table._tablename, "DELETE", authorize=True)
- data = action.uses(db, T)(
+
+ # must wrap into action uses to make sure it closes transactions
+ data = action.uses(db)(
lambda: RestAPI(db, policy)(
request.method, args[2], id, request.query, request.json
)
@@ -376,6 +400,7 @@ def tables(name):
response.status = data["code"]
return data
+
if MODE == "full":
@action("reload")
@@ -384,7 +409,7 @@ def tables(name):
def reload(name=None):
"""Reloads installed apps"""
Reloader.import_app(name) if name else Reloader.import_apps()
- return "ok"
+ return {"status": "ok"}
@action("save/", method="POST")
@session_secured
@@ -393,7 +418,8 @@ def save(path, reload_app=True):
app_name = path.split("/")[0]
path = safe_join(FOLDER, path) or abort()
with open(path, "wb") as myfile:
- myfile.write(request.body.read())
+ body = json.load(request.body)
+ myfile.write(body.encode("utf8"))
if reload_app:
Reloader.import_app(app_name)
return {"status": "success"}
@@ -463,7 +489,6 @@ def new_app():
if process.returncode != 0:
abort(500)
elif form["type"] == "upload":
- print(request.files.keys())
prepare_target_dir(form, target_dir)
source_stream = io.BytesIO(base64.b64decode(form["file"]))
zfile = zipfile.ZipFile(source_stream, "r")
@@ -478,7 +503,10 @@ def new_app():
data = data.replace("", str(uuid.uuid4()))
with open(settings, "w") as fp:
fp.write(data)
- Reloader.import_app(app_name)
+ try:
+ Reloader.import_app(app_name)
+ except Exception:
+ pass
return {"status": "success"}
#
@@ -530,16 +558,22 @@ def gitshow(project, commit):
patch = run("git show " + commit + opt, project)
return diff2kryten(patch)
+
# handle internationalization & pluralization files
#
+
@action("translations/", method="GET")
@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
+
@action("api/translations/", method="GET")
@action.uses(Logged(session))
def get_translations(name):
@@ -547,6 +581,7 @@ def get_translations(name):
t = Translator(os.path.join(FOLDER, name, "translations"))
return t.languages
+
@action("api/translations/", method="POST")
@action.uses(Logged(session))
def post_translations(name):
@@ -566,4 +601,4 @@ def update_translations(name):
"""find all T(...) decorated strings in the code and returns them"""
app_folder = os.path.join(FOLDER, name)
strings = Translator.find_matches(app_folder)
- return {'strings': strings}
+ return {"strings": strings}
diff --git a/apps/_dashboard/diff2kryten.py b/apps/_dashboard/diff2kryten.py
index 8ce9332bc..22866c692 100644
--- a/apps/_dashboard/diff2kryten.py
+++ b/apps/_dashboard/diff2kryten.py
@@ -1,6 +1,5 @@
import sys
-
# Note, when changing the Highlight.js css,
# the background color of .file .diff should match the
# background in the .hljs class in gitlog.min.css
diff --git a/apps/_dashboard/static/components/mtable.html b/apps/_dashboard/static/components/mtable.html
index ed1b7b26b..5a69f5455 100644
--- a/apps/_dashboard/static/components/mtable.html
+++ b/apps/_dashboard/static/components/mtable.html
@@ -2,7 +2,7 @@
-
+
Search
@@ -24,24 +24,24 @@
{{field.label}}
-
-
+
+
True
False
None
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -96,8 +96,8 @@
{{item[field.name]}}
{{item[field.name]}}
{{item[field.name]}}
- {{l2s(item[field.name])}}
- {{l2s(item[field.name])}}
+ {{JSON.stringify(item[field.name])}}
+ {{JSON.stringify(item[field.name])}}
{{(item[field.name]||'').replace('T','@').substr(0,16)}}
{{item[field.name]}}
diff --git a/apps/_dashboard/static/components/mtable.js b/apps/_dashboard/static/components/mtable.js
index 4e2f2f635..7a42bb508 100644
--- a/apps/_dashboard/static/components/mtable.js
+++ b/apps/_dashboard/static/components/mtable.js
@@ -2,7 +2,7 @@
var mtable = { props: ['url', 'filter', 'order', 'editable', 'create', 'deletable', 'render'], data: null, methods: {}};
- mtable.data = function() {
+ mtable.data = function() {
var data = {url: this.url,
busy: false,
filter: this.filter || '',
@@ -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);
});
};
@@ -84,24 +84,11 @@
this.prepare_fields(this.item);
};
- mtable.methods.l2s = function(list) {
- return (list||[]).map(function(x){return ''+x}).join(', ');
- };
-
- mtable.methods.s2l = function(string, is_list_integer) {
- var v = string;
- v = v && v.split(',').map(function(x){return x.trim();}) || [];
- if (is_list_integer) v = v.map(parseInt);
- return v;
- };
-
mtable.methods.prepare_fields = function(item){
let self = this;
for(var field of this.table.model){
- console.log(field.name);
if(field.type == "list:string" || field.type == "list:integer" || field.type.substr(0,14) == "list:reference"){
- self.string_values[field.name] = mtable.methods.l2s(item[field.name]);
- console.log(self.string_values[field.name]);
+ self.string_values[field.name] = JSON.stringify(item[field.name]);
} else if (field.type == "datetime") {
output = field.default != null ? field.default : '';
output = output.split('.')[0];
@@ -113,15 +100,20 @@
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;
});
}
}
}
- }
+ this.$nextTick(function(){
+ Q("input[type=text].type-list-string,input[type=text].type-list-integer,input[type=text].type-list-reference").forEach(
+ function(elem){Q.tags_input(elem);
+ });
+ });
+ };
mtable.methods.parse_and_validate_json = function(event){
try {
@@ -137,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;
}
};
@@ -148,33 +140,40 @@
for(var field of this.table.model) {
var is_list_integer = field.type == "list:integer" || field.type.substr(0,14) == "list:reference";
if(field.type == "list:string" || is_list_integer) {
- item[field.name] = mtable.methods.s2l(this.string_values[field.name], is_list_integer);
+ try {
+ item[field.name] = JSON.parse(this.string_values[field.name]);
+ } catch(err) {
+ alert("Invalid field value: " + field.name);
+ break;
+ }
}
}
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 = [];
- console.log(data);
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;
}
};
};
@@ -207,13 +206,11 @@
window.location = window.location.href.split('?')[0]+'?'+source;
};
-
-
var scripts = document.getElementsByTagName('script');
var src = scripts[scripts.length-1].src;
var path = src.substr(0, src.length-3) + '.html';
- Q.register_vue_component('mtable', path, function(template) {
- mtable.template = template.data;
- return mtable;
- });
+ Q.register_vue_component('mtable', path, function(template) {
+ mtable.template = template.data;
+ return mtable;
+ });
})();
diff --git a/apps/_dashboard/static/css/future.css b/apps/_dashboard/static/css/future.css
index 094c84eb4..e7e0f1ad8 100644
--- a/apps/_dashboard/static/css/future.css
+++ b/apps/_dashboard/static/css/future.css
@@ -236,11 +236,11 @@ label {
border: 2px solid #33BFFF;
padding:10px 20px;
transition: all .5s ease;
- width: 200px;
+ width: 210px;
}
.login input[type=password]:focus {
padding:10px 40px;
- width: 240px;
+ width: 250px;
}
.loading {
z-index: 100;
@@ -284,4 +284,7 @@ label {
}
.field-referenced {
margin-right: 1em;
-}
\ No newline at end of file
+}
+
+.tags-list li { display: inline-block; color: black; background: #d1d1d1; padding: 2px 5px; margin: 5px; border-radius: 5px; }
+.tags-list li[data-selected=false] { opacity: 0.5; }
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/dbadmin.js b/apps/_dashboard/static/js/dbadmin.js
index 97e56c70d..a72d706a1 100644
--- a/apps/_dashboard/static/js/dbadmin.js
+++ b/apps/_dashboard/static/js/dbadmin.js
@@ -1,10 +1,10 @@
-var app = Q.app();
-var params = new URLSearchParams(window.location.search);
+var app = {data: {}, methods:{}};
+app.params = new URLSearchParams(window.location.search);
app.data.loading = 0;
-app.data.app = params.get('app');
-app.data.dbname = params.get('dbname');
-app.data.tablename = params.get('tablename');
-app.data.url = '/_dashboard/rest/{app}/{dbname}/{tablename}'.format(app.data);
-app.data.filter = params.get('filter') || '';
-app.data.order = params.get('order') || '';
-app.start();
+app.data.app = app.params.get('app');
+app.data.dbname = app.params.get('dbname');
+app.data.tablename = app.params.get('tablename');
+app.data.url = '../rest/{app}/{dbname}/{tablename}'.format(app.data);
+app.data.filter = app.params.get('filter') || '';
+app.data.order = app.params.get('order') || '';
+app.vue = new Vue({el:"#vue", data: app.data, methods: app.methods});
diff --git a/apps/_dashboard/static/js/index.js b/apps/_dashboard/static/js/index.js
index 595b63697..9b0d1b515 100644
--- a/apps/_dashboard/static/js/index.js
+++ b/apps/_dashboard/static/js/index.js
@@ -33,9 +33,7 @@ let init = (app) => {
};
app.select_app = (appobj) => {
app.vue.selected_app = appobj;
- app.vue.walk = [];
- axios.get('../walk/'+appobj.name).then((res)=>{app.vue.walk=res.data.payload;});
- axios.get('../rest/'+appobj.name).then((res)=>{app.vue.databases=res.data.databases;});
+ app.reload_files();
};
app.activate_editor = (path, payload) => {
app.vue.files[path] = payload;
@@ -70,8 +68,8 @@ let init = (app) => {
} else {
var url = '../load/'+path;
if(app.vue.selected_type != 'text') url = '../load_bytes/'+path;
- axios.get(url).then((res)=>{
- app.activate_editor(path, res.data.payload);
+ Q.get(url).then(r=>{
+ app.activate_editor(path, r.json().payload);
});
}
}
@@ -83,7 +81,7 @@ let init = (app) => {
app.reload = (name) => {
app.modal_dismiss();
app.vue.loading = true;
- axios.get(name?'../reload/'+name:'../reload').then(app.init);
+ Q.get(name?'../reload/'+name:'../reload').then(r=>app.init());
};
app.gitlog = (name) => {
window.open('../gitlog/'+name);
@@ -96,11 +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();
- axios.post('../save/'+path, app.vue.files[path]).then(()=>{
- 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()
@@ -121,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 {
- axios.post('../new_app', form).then(app.reload);
+ Q.post('../new_app', form).then(r=>app.reload());
}
};
app.process_new_file = () => {
@@ -129,14 +129,14 @@ 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*/
- axios.post('../new_file/'+app_name+'/'+form.filename).then(function(){
+ Q.post('../new_file/'+app_name+'/'+form.filename).then(r=>{
app.vue.walk = [];
- axios.get('../walk/'+app_name).then((res)=>{app.vue.walk=res.data.payload;});
+ Q.get('../walk/'+app_name).then(r=>{app.vue.walk=r.json().payload;});
app.modal_dismiss();
});
};
app.handle_upload_file = () => {
- Q.upload_helper('upload-file', (name, data)=>{app.vue.modal.form.file=data;});
+ Q.upload_helper('upload-file', (name, data)=>{app.vue.modal.form.file=data; app.vue.modal.form.type = "upload";});
};
app.upload_new_app = ()=> {
app.vue.modal = {
@@ -167,57 +167,64 @@ let init = (app) => {
app.delete_selected_file = () => {
var name = app.vue.selected_filename;
app.confirm("Delete File","blue","Do you really want to delete "+name+"?",()=>{
- app.modal_dismiss();
- axios.post('../delete/'+name).then(()=>{
- app.init();
- });
- });
+ app.modal_dismiss();
+ 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();
- axios.post('../delete_app/'+name).then(()=>{
- app.init();
- });
- });
+ app.modal_dismiss();
+ Q.post('../delete_app/'+name).then(r=>app.init());
+ });
};
app.reload_info = () => {
- axios.get('../info').then((res)=>{
- app.vue.info=res.data.payload || [];
- });
+ Q.get('../info').then(r=>{
+ app.vue.info=r.json().payload || [];
+ });
};
app.reload_apps = () => {
- axios.get('../apps').then((res)=>{
- app.vue.apps=res.data.payload || []; app.update_selected();
- });
+ Q.get('../apps').then(r=>{
+ app.vue.apps=r.json().payload || []; app.update_selected();
+ });
};
app.reload_routes = () => {
- axios.get('../routes').then((res)=>{
- app.vue.routes=res.data.payload || [];
+ Q.get('../routes').then(r=>{
+ app.vue.routes=r.json().payload || [];
});
};
app.reload_tickets = () => {
app.vue.tickets = [];
- axios.get('../tickets').then((res)=>{
- app.vue.tickets = res.data.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 = [];
- axios.get('../clear').then(app.reload_tickets());
+ Q.get('../clear').then(r=>app.reload_tickets());
};
app.login = () => {
- axios.post('../login',{'password': app.vue.password}).then(function(res){
+ Q.post('../login', {'password': app.vue.password})
+
+ .then(r=>{
app.vue.password = '';
- if( res.data.user) {
+ if( r.user) {
app.vue.user = true;
app.init();
}
+ location.reload();
});
};
app.logout = () => {
- axios.post('../logout').then(()=>{window.location.reload();});
+ Q.post('../logout').then(r=>window.location.reload());
};
app.methods = {
select: app.select_app,
@@ -252,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 8873d808b..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);
@@ -56,26 +62,6 @@ Q.get_cookie = function (name) {
return decodeURIComponent(!!cookie ? cookie.toString().replace(/^[^=]+./, "") : "");
};
-// Gets a session token (py4web specific)
-Q.get_session_token = function () {
- var app_name = Q.get_cookie('app_name');
- return Q.get_cookie(app_name + '_session');
-};
-
-// Load data from localstorage
-Q.retrieve = function (key) {
- try {
- return JSON.parse(window.localStorage.getItem(key));
- } catch (e) {
- return null;
- }
-};
-
-// Save data to localstorage
-Q.store = function (key, value) {
- window.localStorage.setItem(key, JSON.stringify(value));
-};
-
// Load components lazily: https://vuejs.org/v2/guide/components.html#Async-Components
Q.register_vue_component = function (name, src, onload) {
Vue.component(name, function (resolve, reject) {
@@ -164,59 +150,6 @@ Q.throttle = (callback, delay) => {
return throttledEventHandler;
};
-// A Vue app prototype
-Q.app = function (elem_id) {
- self = {};
- self.elem_id = elem_id || 'vue';
- self.data = { loading: 0, page: null, state: null };
- self.methods = {};
- self.filters = {};
- self.watch = {};
- self.pages = {};
- // translations
- self.methods.T = T;
- // toggles a variable
- self.methods.toggle = function (obj, key) { obj[key] = !obj[key] };
- // sets a variable
- self.methods.set = function (obj, key, value) { obj[key] = value; };
- // goto a given page and state (state should be 1 level deep dict
- self.methods.go = function (page, state, push) {
- self.v.loading++;
- var pagecall = self.pages[page];
- if (pagecall) pagecall(state, function () {
- if (push) {
- var path = self.base + '/' + page;
- if (state) for (var key in state) path += '/' + key + '/' + state[key];
- window.history.pushState(self.v, page, path);
- }
- self.v.loading--;
- self.v.page = page;
- self.v.state = state;
- });
- };
- // restores state when navigating history
- self.onpopstate = function (event) {
- for (var key in event.state) self.v[key] = event.state[key];
- };
- self.start = function (base) {
- self.base = base = base || window.location.href;;
- self.v = new Vue({
- el: '#' + self.elem_id,
- data: self.data,
- methods: self.methods,
- watch: self.watch,
- filters: self.filters
- });
- var parts = window.location.href.substr(base.length);
- var page = parts[0];
- var state = {};
- for (var i = 1; i < parts.length; i += 2) state[parts[i]] = parts[i + 1];
- self.v.go(page, state, false);
- window.onpopstate = self.onpopstate;
- };
- return self;
-};
-
// Renders a JSON field with tags_input
Q.tags_input = function(elem, options) {
if (typeof elem === typeof '') elem = Q(elem)[0];
@@ -245,7 +178,6 @@ Q.tags_input = function(elem, options) {
var fill = function(elem, repl) {
repl.innerHTML = '';
tags.forEach(function(x){
- console.log(x);
var item = document.createElement('li');
item.innerHTML = options.labels[x] || x;
item.dataset.value = x;
@@ -255,12 +187,14 @@ Q.tags_input = function(elem, options) {
if(item.dataset.selected=='false') keys.push(x); else keys = keys.filter(function(y){ return x!=y; });
item.dataset.selected = keys.indexOf(x)>=0;
elem.value = JSON.stringify(keys);
+ elem.dispatchEvent(new Event('input', { bubbles: true }));
};
});
};
if (options.freetext) {
var inp = document.createElement('input');
elem.parentNode.insertBefore(inp, elem);
+ inp.type = "text";
inp.classList = elem.classList;
inp.placeholder = options.placeholder;
inp.setAttribute('list', options.autocomplete_list);
@@ -273,6 +207,7 @@ Q.tags_input = function(elem, options) {
});
inp.value = '';
elem.value = JSON.stringify(keys);
+ elem.dispatchEvent(new Event('input', { bubbles: true }));
fill(elem, repl);
};
}
@@ -330,7 +265,6 @@ Q.load_and_trap = function (method, url, form_data, target) {
if (res.redirected) window.location = res.url;
Q('#'+target)[0].innerHTML = res.data;
Q.trap_form(url, target);
- console.log(res.headers);
var flash = res.headers.get('component-flash');
if (flash) Q.flash(JSON.parse(flash));
};
@@ -368,7 +302,6 @@ Q.handle_flash = function() {
if (elem) {
elem.addEventListener('flash', make_handler(elem), false);
Q.flash = function(detail) {elem.dispatchEvent(new CustomEvent('flash', {detail: detail}));};
- console.log(elem.dataset.alert);
if (elem.dataset.alert) Q.flash(Q.eval(elem.dataset.alert));
}
};
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/_dashboard/templates/translations.html b/apps/_dashboard/templates/translations.html
index 9d4f38815..8f69c0508 100644
--- a/apps/_dashboard/templates/translations.html
+++ b/apps/_dashboard/templates/translations.html
@@ -60,7 +60,6 @@
-
diff --git a/apps/_dashboard/utils.py b/apps/_dashboard/utils.py
index 37b2cc25f..3a602b130 100644
--- a/apps/_dashboard/utils.py
+++ b/apps/_dashboard/utils.py
@@ -9,20 +9,18 @@
---------------
"""
+import glob
+import gzip
+import logging
import os
import re
-import tarfile
-import glob
import shutil
-import logging
-import gzip
-
+import tarfile
__all__ = (
"safe_join",
"list_dir",
"recursive_unlink",
- "sanitize",
"tar",
"untar",
"pack",
@@ -89,13 +87,6 @@ def recursive_unlink(path):
os.unlink(path)
-def sanitize(name):
- """Turns any expression/path into a valid filename. replaces / with _ and
- removes special characters.
- """
- return re.sub(r"\W", "", re.sub("[/.-]+", "_", name))
-
-
def _extractall(filename, path=".", members=None):
tar = tarfile.TarFile(filename, "r")
tar.extractall(path, members)
diff --git a/apps/_default/__init__.py b/apps/_default/__init__.py
index b5d43cbb0..49d739257 100644
--- a/apps/_default/__init__.py
+++ b/apps/_default/__init__.py
@@ -1,7 +1,12 @@
-from py4web import action, __version__
+import os
+from py4web import Cache, action
+
+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 @@
+
+