Compare commits

...

101 Commits

Author SHA1 Message Date
mdipierro 5583e9cdc7 R-2.16.0b1 2017-07-05 01:37:48 -05:00
mdipierro 5d8a25626c tracking PyDAL 17.07 2017-07-05 01:32:50 -05:00
mdipierro 9ded289924 fixed minor pylint -E errors 2017-07-02 01:55:51 -05:00
mdipierro f657b42f65 fixed undefined variable 2017-07-02 01:34:05 -05:00
mdipierro 1c0b498880 fixed undefined variable 2017-07-02 01:32:25 -05:00
mdipierro 2fd0c7c778 new dal 2017-06-30 09:22:50 -05:00
mdipierro 3d0b81cbe6 reverted backward imcompatible change 2017-06-30 08:22:26 -05:00
mdipierro 48d69e9724 Merge branch 'master' of github.com:web2py/web2py 2017-06-27 16:43:58 -05:00
mdipierro 757ce4e76e fixes #1528 2017-06-27 16:43:42 -05:00
mdipierro 2f0b429f9e Merge pull request #1651 from BuhtigithuB/improve/pep8-authapi-py
Enhance authapi.py PEP8 compliancy
2017-06-20 16:44:18 -05:00
Richard Vézina a78662e4cc Enhance authapi.py PEP8 compliancy 2017-06-20 16:02:50 -04:00
mdipierro 81fa787ec2 fixes #1514, thanks RekGRpth 2017-06-20 14:59:57 -05:00
mdipierro 159dd0d022 fixed #1542, thanks RekGRpth 2017-06-20 14:49:54 -05:00
mdipierro 16df6840ed fixes #1547, thanks yaminle 2017-06-20 14:47:59 -05:00
mdipierro 0674111129 fixes #1579, thanks Nico 2017-06-20 14:29:47 -05:00
mdipierro 18b755b8da fixed #1583, thanks matclab 2017-06-20 14:24:35 -05:00
mdipierro 2174fc3bec partially fixes #1621, thanks Nico. The strip issue is a python3 error in my opinion 2017-06-20 12:26:14 -05:00
mdipierro 77a947b35c fixed #1638, thanks Anthony 2017-06-20 12:02:40 -05:00
mdipierro 954cef48da fixes #1648, thanks you tbear 2017-06-20 11:53:47 -05:00
mdipierro 566feb79d4 Merge pull request #1649 from ilvalle/fix_globals
fix Pickler.dispatch_table
2017-06-15 04:01:14 -05:00
mdipierro c117e3d1b9 Merge pull request #1644 from leonelcamara/test_is_url_py3
Changed URL validation to use urlparse instead of regex for spliting …
2017-06-15 04:00:30 -05:00
mdipierro 0c3662fb6d scheduler should decode credentials 2017-06-15 03:59:51 -05:00
ilvalle 2ea5939640 fix Pickler.dispatch_table 2017-06-14 21:45:18 +02:00
Leonel Câmara 7c9653ae23 Merge branch 'master' into test_is_url_py3 2017-06-14 15:41:07 +01:00
mdipierro a02538549b Merge pull request #1642 from leonelcamara/restful
Fixes #1634 by adding a parameter "ignore_extension" to Request.restf…
2017-06-14 09:30:52 -05:00
mdipierro ce965cf62a Merge pull request #1641 from leonelcamara/i1639
Fixes #1639
2017-06-14 09:30:25 -05:00
Leonel Câmara 2a33c0faff Changed URL validation to use urlparse instead of regex for spliting the URL
Enabled test_is_url in Python 3 since it is now passing
This might be one of the last fixes to #1353
Fixes #1598
2017-06-07 04:59:03 +01:00
Leonel Câmara ac67beb280 Fixes #1634 by adding a parameter "ignore_extension" to Request.restful which allows you to leave the args untouched if you set it to True. 2017-06-06 01:53:42 +01:00
Leonel Câmara 617ca4a98d Fixes #1639 2017-06-05 23:52:25 +01:00
mdipierro 85ecebc3a4 Merge pull request #1640 from leonelcamara/i1628better
Fixes #1628
2017-06-05 17:44:01 -05:00
Leonel Câmara 376c12a225 Fixes #1628 2017-06-05 23:35:41 +01:00
mdipierro 7769917102 Merge pull request #1637 from timrichardson/issues/1636
fix to #1636: sqlgrid left= (left outer join) fails because db._adapt…
2017-06-05 11:48:33 -05:00
mdipierro 30c28ad44c Merge pull request #1630 from ilvalle/fix_1629
fix web debugger
2017-06-05 11:41:25 -05:00
mdipierro d905968197 Merge pull request #1626 from willimoa/issue/1472
Issue/1472
2017-06-05 11:40:38 -05:00
mdipierro f1ef95e15f Merge pull request #1625 from Scimonster/master
Fix #1624 -- Unicode in XML sanitizing causes error
2017-06-05 11:39:52 -05:00
mdipierro da6688360d Merge pull request #1623 from ilvalle/fix_tag_py3
fix TAG helper on PY3
2017-06-05 11:39:15 -05:00
Tim Richardson f30c31a8a2 fix to #1636: sqlgrid left= (left outer join) fails because db._adapter.tables() now returns dict (previously a table) 2017-06-01 16:53:04 +10:00
Andrew Willimott 59405b9f18 d3 js check was pointing to app, now checking in admin app. 2017-05-16 06:54:51 +12:00
Andrew Willimott 1c747357b0 Repointed appadmin to admin/static for d3 files 2017-05-16 06:47:30 +12:00
Andrew Willimott 6342bf0ddb Consistent set of d3 js and css across admin, examples, and welcome 2017-05-15 20:12:58 +12:00
ilvalle f76437703b fix web debugger, close #1629 2017-05-13 08:26:09 +02:00
Andrew Willimott 6f256be1f1 Added databases variable check 2017-05-08 07:02:31 +12:00
Andrew Willimott 3505e372d8 Remove graphviz graph code from appadmin and design files 2017-05-08 06:40:50 +12:00
Scimonster 49bf14e79a Fix #1624 -- Unicode in XML sanitizing causes error
Add unit test for it
2017-05-07 14:32:52 +03:00
ilvalle cf1ea98217 fix TAG helper on PY3, updated web2pyHTMLParser 2017-05-06 08:51:58 +02:00
mdipierro 8a741023d8 Merge pull request #1622 from willimoa/enhancement/d3_graph
Enhancement/d3 graph
2017-05-03 23:57:42 -05:00
mdipierro 1f4a490a84 Merge pull request #1620 from ilvalle/issue_1618
fix cron with py3, added initial tests, close #1618
2017-05-03 23:57:12 -05:00
Andrew Willimott ca5539561f Typo delete fix for hooks() return 2017-05-04 07:17:04 +12:00
Andrew Willimott 57b554d618 Initial d3 graph commit. Add d3 to graph layout 2017-05-04 07:03:07 +12:00
mdipierro baa129f871 Merge pull request #1527 from leonelcamara/authapi2
Auth refactor
2017-05-01 09:13:18 -05:00
ilvalle 90e606dcfd fix cron with py3, added initial tests, close #1618 2017-05-01 15:31:37 +02:00
mdipierro 1d77968a06 Merge pull request #1617 from ilvalle/issue_1609
fix open file in py3
2017-04-28 08:12:54 -05:00
mdipierro 1842a2e42c Merge pull request #1616 from cccaballero/py3welcome
Fixes in generic layouts for python 3 compatibility
2017-04-28 08:11:42 -05:00
mdipierro c9b6b0faf8 Merge pull request #1607 from sugizo/fix_memcache_appadmin
appadmin can use ccache with cache.ram = cache.disk = cache.memcache
2017-04-28 08:09:17 -05:00
mdipierro d8be963656 Merge pull request #1606 from cccaballero/master
Added on_succes and custom options for web2py ajax function
2017-04-28 08:08:39 -05:00
mdipierro 496112c4fe Merge pull request #1604 from ilvalle/fix_1570
prevent is_empty from stripping whitespaces, close #1570
2017-04-28 08:07:20 -05:00
mdipierro a186f5c51f Merge pull request #1603 from ilvalle/fix_1582_internazionalized_domain_names
fix IS_EMAIL for internationalized domain names
2017-04-28 08:05:30 -05:00
mdipierro fef43cd053 Merge pull request #1602 from leonelcamara/fix_webclientforget
Create a CookieJar on __init__ instead of creating a new one each post
2017-04-28 08:04:56 -05:00
mdipierro ab41cd94ec Merge pull request #1601 from leonelcamara/port_dialog
Make web2pyDialog Python 3 compatible
2017-04-28 08:04:06 -05:00
mdipierro 9ab7ed0029 catching errors in python3 in custom_import 2017-04-28 08:01:20 -05:00
ilvalle 4d117af85f fix open file in py3, close #1609 2017-04-27 18:08:59 +02:00
Carlos Cesar Caballero Díaz 60a6180a77 fixes in generic layouts for python 3 compatibility 2017-04-27 10:35:27 -04:00
sugizo ab537242c4 appadmin can use ccache with cache.ram = cache.disk = cache.memcache 2017-04-13 06:31:40 +07:00
Carlos Cesar Caballero Díaz 02d2fefc21 renamed "on_succes" option to "done" allowing multiple inputs
renamed "on_succes" option to "done" allowing multiple inputs,code
refactoring
2017-04-12 08:48:52 -04:00
Carlos Cesar Caballero Díaz 232598fd8d Added on_succes and custom options for web2py ajax function 2017-04-10 12:23:34 -04:00
ilvalle 7a69e087f7 prevent is_empty from stripping whitespaces, close #1570 2017-04-06 20:48:12 +02:00
ilvalle 6da3a9d8fd fix is_email with internationalized Domain Names, close #1582 2017-04-05 22:16:21 +02:00
Leonel Câmara fe0f506efc Create a CookieJar on __init__ instead of creating a new one each post
Fixes #1505
2017-04-03 19:54:06 +01:00
Leonel Câmara 140023e920 Make web2pyDialog Python 3 compatible
checks "port web2pyDialog" in #1353
2017-03-31 12:04:55 +01:00
mdipierro ad43249f61 Merge branch 'master' of github.com:web2py/web2py 2017-03-21 12:28:14 -05:00
mdipierro dd31fb480c Merge branch 'ilvalle-py36' 2017-03-21 12:27:29 -05:00
mdipierro e5121876db Merge branch 'py36' of https://github.com/ilvalle/web2py into ilvalle-py36 2017-03-21 12:27:11 -05:00
mdipierro 532137bce5 Merge pull request #1594 from BuhtigithuB/improve/translations
Improve french translation and close #1592
2017-03-21 12:24:44 -05:00
mdipierro 35f7caa2f0 Merge pull request #1591 from cccaballero/master
Some fixes on ldap_auth
2017-03-21 12:24:05 -05:00
mdipierro 98dddee697 Merge pull request #1578 from ndegroot/minorcleanupbs3
minor cleanup removed unused class 'navbar-ex1-collapse' see https://…
2017-03-21 12:22:48 -05:00
mdipierro 5000c47472 Merge pull request #1568 from amerikan/patch-2
Remove ! from tag
2017-03-21 12:22:16 -05:00
mdipierro bf3d53ad96 Merge pull request #1567 from amerikan/patch-3
Removed ! from tag
2017-03-21 12:21:13 -05:00
mdipierro b2f221bbaa Merge pull request #1566 from BrenBarn/flexserve
Add FlexibleService, which allows @service-style methods with varargs
2017-03-21 12:18:09 -05:00
mdipierro 868d6f6369 Merge branch 'master' of github.com:web2py/web2py 2017-03-21 12:06:11 -05:00
mdipierro 0e1831bcc7 Merge pull request #1581 from nextghost/pydal-17.01
Update SQLTABLE for API changes in PyDAL 17.01
2017-03-21 12:06:00 -05:00
mdipierro dda808ebda pydal 17.03 2017-03-21 12:05:47 -05:00
Hardirc a81e116274 Improve french translation and close #1592 2017-03-15 01:03:21 -04:00
Carlos Cesar Caballero Díaz 34f2825a49 fix ldap_auth logging info not shown 2017-03-14 13:09:29 -04:00
Carlos Cesar Caballero Díaz 7dec909254 fixed group mapping on ldap auth 2017-03-14 13:00:32 -04:00
ilvalle ca9198a26e updated to latest dal 2017-03-07 21:11:05 +01:00
ilvalle ad421e42c6 added tests for py36 2017-03-06 21:35:23 +01:00
Martin Doucha 6954988851 Update SQLTABLE for API changes in PyDAL 17.01 2017-02-28 22:12:54 +01:00
Nico de Groot f0382d646c minor cleanup removed unused class 'navbar-ex1-collapse' see https://github.com/twbs/bootstrap/issues/10948 2017-02-26 13:21:44 +01:00
Erik Montes 48a0683dd1 Removed ! from tag 2017-02-07 10:47:26 -08:00
Erik Montes d7fb270e7e Remove ! from tag 2017-02-07 10:46:22 -08:00
BrenBarn 86a2c529b9 Change to modify Service instead of adding FlexibleService 2017-01-31 14:13:43 -08:00
BrenBarn 55592e7c6e Add FlexibleService, which allows @service-style methods that accept varargs 2017-01-31 11:48:28 -08:00
Leonel Câmara a23b264a37 make sudo false again 2016-11-21 19:48:41 +00:00
Leonel Câmara 79e256b1d7 require sudo 2016-11-21 15:53:10 +00:00
Leonel Câmara 920ab72415 fixed _update_session_user not really updating the session 2016-11-20 20:11:45 +00:00
Leonel Câmara 757d46274e fix bug with new login_user 2016-11-20 20:00:51 +00:00
Leonel Câmara 7b66ec0ae3 Fixes #1506 2016-11-20 19:51:51 +00:00
Leonel Câmara bf5ec0d7cf Fixed a long standing bug in login_user which was using 'password' instead of settings.password_field
Fixes #636
2016-11-20 19:38:21 +00:00
Leonel Câmara 85c68e6876 typo 2016-11-05 16:52:42 +00:00
Leonel Câmara d1dfc4a06a use _compat's long 2016-11-05 16:51:54 +00:00
Leonel Câmara 02f0bdb8d3 Auth refactor, extracted many methods into a base class for more generic auth mechanisms.
Partially addresses #1526
Includes a solution for IS_LOWER and IS_UPPER validator problems I mentioned in #1353
2016-11-05 16:37:22 +00:00
65 changed files with 2833 additions and 1699 deletions
+1
View File
@@ -8,6 +8,7 @@ python:
- '2.7'
- 'pypy'
- '3.5'
- '3.6'
install:
- pip install -e .
+33
View File
@@ -1,3 +1,36 @@
## 2.16.0b1
- experimental python 3 support
- experimental authapi for service login
- more tests
- d3.js model visulization
- improved scheduler
- is_email support for internationalized Domain Names
- improved used of cookies with CookieJar
- SQLFORM.grid(showblobs=True)
- import JS events (added w2p.componentBegin event)
- added support for CASv3
- allow first_name and last_name placeholders in verify_email message
- added three-quote support in markmin
- updated pg8000 driver (but we still recommend psycopg2)
- compiled views use . separator not _ separator (must recompile code)
- better serbian, french, and catalan translations
- speed improvements (refactor of compileapp and pyc caching)
- removed web shell (never worked as intended)
- allow Expose(..., follow_symlink_out=False).
- Updated fpdf to latest version
- JWT support
- import fabfile for remote deployment
- jQuery 3.2.1
- PyDAL 17.07 including:
allow jsonb support for postgres
correctly configure adapters that need connection for configuration
better caching
updated IMAP adapter methods to new API
experimental suport for joinable subselects
improved Teradata support
improved mongodb support
overall refactoring
experimental support for Google Cloud SQL v2 (TODO)
## 2.15.x
- web2py does not support python 2.6 anymore
+49 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -565,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -700,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('static','js/d3.min.js'))
response.files.append(URL('static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
+2 -1
View File
@@ -6,6 +6,7 @@ import gluon.validators
import code
from gluon.debug import communicate, web_debugger, dbg_debugger
from gluon._compat import thread
from gluon.fileutils import open_file
import pydoc
@@ -54,7 +55,7 @@ def interact():
if filename:
# prevent IOError 2 on some circuntances (EAFP instead of os.access)
try:
lines = open(filename).readlines()
lines = open_file(filename, 'r').readlines()
except:
lines = ""
lines = dict([(i + 1, l) for (i, l) in enumerate(
+4 -1
View File
@@ -16,6 +16,7 @@ from gluon.tools import Config
from gluon.compileapp import find_exposed_functions
from glob import glob
from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native
import gluon.rewrite
import shutil
import platform
@@ -249,6 +250,7 @@ def site():
db.app.insert(name=appname, owner=auth.user.id)
log_progress(appname)
session.flash = T('new application "%s" created', appname)
gluon.rewrite.load()
redirect(URL('design', args=appname))
else:
session.flash = \
@@ -266,6 +268,7 @@ def site():
new_repo = git.Repo.clone_from(form_update.vars.url, target)
session.flash = T('new application "%s" imported',
form_update.vars.name)
gluon.rewrite.load()
except git.GitCommandError as err:
session.flash = T('Invalid git repository specified.')
redirect(URL(r=request))
@@ -302,6 +305,7 @@ def site():
log_progress(appname)
session.flash = T(msg, dict(appname=appname,
digest=md5_hash(installed)))
gluon.rewrite.load()
else:
msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -1861,7 +1865,6 @@ def user():
def reload_routes():
""" Reload routes.py """
import gluon.rewrite
gluon.rewrite.load()
redirect(URL('site'))
+138 -137
View File
@@ -21,18 +21,19 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'@markmin\x01Searching: **%s** %%{file}': 'Cherche: **%s** fichiers',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'A new version of web2py is available: %s': 'Une nouvelle version de web2py est disponible: %s ',
'A new version of web2py is available: Version 1.68.2 (2009-10-21 09:59:29)\n': 'Une nouvelle version de web2py est disponible: Version 1.68.2 (2009-10-21 09:59:29)\r\n',
'Abort': 'Abort',
'About': 'à propos',
'About application': "A propos de l'application",
'About': 'À propos',
'About application': "À propos de l'application",
'Accept Terms': 'Termes acceptés',
'Add breakpoint': 'Ajouter une interruption',
'additional code for your application': 'code supplémentaire pour votre application',
'Additional code for your application': 'Code additionnel pour votre application',
'Admin design page': 'Admin design page',
'Admin design page': 'Page de conception admin',
'admin disabled because no admin password': 'admin désactivée car aucun mot de passe admin',
'admin disabled because not supported on google app engine': 'admin désactivée car non prise en charge sur Google Apps engine',
'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts',
@@ -41,7 +42,7 @@
'Admin language': "Language de l'admin",
'Admin versioning page': 'Admin versioning page',
'administrative interface': "interface d'administration",
'Administrator Password:': 'Mot de passe Administrateur:',
'Administrator Password:': "Mot de passe de l'administrateur:",
'An error occured, please [[reload %s]] the page': 'Une erreur cest produite, sil vous plait [[reload %s]] la page',
'and rename it (required):': 'et renommez-la (obligatoire):',
'and rename it:': 'et renommez-le:',
@@ -57,14 +58,14 @@
'application is compiled and cannot be designed': "l'application est compilée et ne peut être modifiée",
'Application name:': "Nom de l'application:",
'Application updated via git pull': 'Application updated via git pull',
'are not used': 'are not used',
'are not used yet': 'are not used yet',
'are not used': 'ne sont pas utilisé',
'are not used yet': 'ne sont pas encore utilisé',
'Are you sure you want to delete file "%s"?': 'Êtes-vous sûr de vouloir supprimer le fichier «%s»?',
'Are you sure you want to delete plugin "%s"?': 'Êtes-vous sûr de vouloir supprimer le plugin "%s"?',
'Are you sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
'Are you sure you want to uninstall application "%s"?': "Êtes-vous sûr de vouloir désinstaller l'application «%s»?",
'Are you sure you want to upgrade web2py now?': 'Êtes-vous sûr de vouloir mettre à jour web2py maintenant?',
'Are you sure?': 'Etes vous sûr?',
'Are you sure?': 'Êtes vous sûr?',
'arguments': 'arguments',
'at char %s': 'at char %s',
'at line %s': 'at line %s',
@@ -73,13 +74,13 @@
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: les tests ne sont pas thread-safe DONC NE PAS EFFECTUER DES TESTS MULTIPLES SIMULTANÉMENT.',
'ATTENTION: you cannot edit the running application!': "ATTENTION: vous ne pouvez pas modifier l'application qui tourne!",
'Autocomplete Python Code': 'Autocomplete Python Code',
'Available databases and tables': 'Bases de données et tables disponible',
'Available databases and tables': 'Bases de données et tables disponibles',
'Available Databases and Tables': 'Available Databases and Tables',
'back': 'retour',
'Back to the plugins list': 'Retour à la liste de plugins',
'Back to wizard': 'Back to wizard',
'Basics': 'Basics',
'Begin': 'Begin',
'Begin': 'Début',
'breakpoint': 'breakpoint',
'Breakpoints': 'Breakpoints',
'breakpoints': 'breakpoints',
@@ -92,7 +93,7 @@
'Cache Keys': 'Cache Keys',
'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyés',
'can be a git repo': 'can be a git repo',
'Cancel': 'Retour',
'Cancel': 'Annuler',
'Cannot be empty': 'Ne peut pas être vide',
'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Ne peut pas compiler: il y a des erreurs dans votre application. corriger les erreurs et essayez à nouveau.',
'Cannot compile: there are errors in your app:': 'Ne peut pas compiler: il y a des erreurs dans votre application:',
@@ -102,60 +103,60 @@
'Change admin password': 'Changer le mot de passe admin',
'change editor settings': 'change editor settings',
'Changelog': 'Changelog',
'check all': 'tout vérifier ',
'check all': 'tout sélectionner',
'Check for upgrades': 'Vérifier les mises à jour',
'Check to delete': 'Cocher pour supprimer',
'Checking for upgrades...': 'Vérification des mises à jour ... ',
'Clean': 'nettoyer',
'Clear': 'Clear',
'Clear CACHE?': 'Clear CACHE?',
'Clear DISK': 'Clear DISK',
'Clear RAM': 'Clear RAM',
'Clear': 'Effacer',
'Clear CACHE?': 'Effacer le CACHE?',
'Clear DISK': 'Effacer le DISQUE',
'Clear RAM': 'Effacer la RAM',
'Click row to expand traceback': 'Click row to expand traceback',
'Click row to view a ticket': 'Click row to view a ticket',
'click to check for upgrades': 'Cliquez pour vérifier les mises jour',
'code': 'code',
'Code listing': 'Code listing',
'collapse/expand all': 'tout réduire/agrandir',
'Command': 'Command',
'Comment:': 'Comment:',
'Commit': 'Commit',
'Commit form': 'Commit form',
'Committed files': 'Committed files',
'Command': 'Commande',
'Comment:': 'Commentaire:',
'Commit': 'Valider',
'Commit form': 'Valider le formulaire',
'Committed files': 'Fichiers validés',
'Compile': 'compiler',
'Compile (all or nothing)': 'Compile (all or nothing)',
'Compile (skip failed views)': 'Compile (skip failed views)',
'compiled application removed': 'application compilée enlevée',
'Condition': 'Condition',
'continue': 'continue',
'continue': 'continuer',
'Controllers': 'Contrôleurs',
'controllers': 'contrôleurs',
'Count': 'Count',
'Count': 'Compte',
'Create': 'Créer',
'create file with filename:': 'créer un fichier avec nom de fichier:',
'create new application:': 'créer une nouvelle application:',
'Create new simple application': 'Créer une nouvelle application',
'Create/Upload': 'Create/Upload',
'created by': 'créé par',
'Created by:': 'Created by:',
'Created On': 'Created On',
'Created on:': 'Created on:',
'Created by:': 'Créé par:',
'Created On': 'Créé le',
'Created on:': 'Créé le:',
'crontab': 'crontab',
'Current request': 'Requête actuelle',
'Current response': 'Réponse actuelle',
'Current session': 'Session en cours',
'currently running': 'tourne actuellement',
'currently saved or': 'actuellement enregistré ou',
'data uploaded': 'données chargées',
'Database': 'Database',
'data uploaded': 'données téléversées',
'Database': 'Base de données',
'database': 'base de données',
'Database %s select': 'Database %s select',
'database %s select': 'base de données %s sélectionner',
'Database administration': 'Database administration',
'database %s select': 'base de données %s sélectionner',
'Database administration': 'Administration base de données',
'database administration': 'administration base de données',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'Date and Time': 'Date et heure',
'db': 'bdd',
'db': 'bd',
'Debug': 'Debug',
'defines tables': 'définit les tables',
'Delete': 'Supprimer',
@@ -165,7 +166,7 @@
'Delete this file (you will be asked to confirm deletion)': 'Supprimer ce fichier (on vous demandera de confirmer la suppression)',
'Delete:': 'Supprimer:',
'deleted after first hit': 'deleted after first hit',
'Demo': 'Demo',
'Demo': 'Démo',
'Deploy': 'Déployer',
'Deploy on Google App Engine': 'Déployer sur Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift',
@@ -176,22 +177,22 @@
'Description:': 'Description:',
'design': 'conception',
'Detailed traceback description': 'Detailed traceback description',
'details': 'details',
'details': 'détails',
'direction: ltr': 'direction: ltr',
'directory not found': 'directory not found',
'Disable': 'Disable',
'Disabled': 'Disabled',
'Disable': 'Désactiver',
'Disabled': 'Désactivé',
'disabled in demo mode': 'disabled in demo mode',
'disabled in GAE mode': 'disabled in GAE mode',
'disabled in multi user mode': 'disabled in multi user mode',
'DISK': 'DISK',
'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
'Disk Cleared': 'Disque effacé',
'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Display line numbers': 'Display line numbers',
'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.',
'docs': 'docs',
'Docs': 'Docs',
'docs': 'documents',
'Docs': 'Documents',
'done!': 'fait!',
'Downgrade': 'Downgrade',
'Download .w2p': 'Download .w2p',
@@ -201,43 +202,43 @@
'download plugins': 'télécharger plugins',
'Download plugins from repository': 'Download plugins from repository',
'EDIT': 'MODIFIER',
'Edit': 'modifier',
'edit all': 'edit all',
'Edit': 'Modifier',
'edit all': 'tout modifier',
'Edit application': "Modifier l'application",
'edit controller': 'modifier contrôleur',
'edit controller:': 'edit controller:',
'Edit current record': 'Modifier cette entrée',
'edit views:': 'modifier vues:',
'Editing %s': 'Editing %s',
'edit controller:': 'modifier le contrôleur:',
'Edit current record': 'Modifier cet enregistrement',
'edit views:': 'modifier les vues:',
'Editing %s': 'Modifier %s',
'Editing file': 'Modifier le fichier',
'Editing file "%s"': 'Modifier le fichier "% s" ',
'Editing Language file': 'Modifier le fichier de langue',
'Editing Plural Forms File': 'Editing Plural Forms File',
'Editor': 'Editor',
'Email Address': 'Email Address',
'Enable': 'Enable',
'Editing Plural Forms File': 'Modifier le fichier du formulaire pluriel',
'Editor': 'Éditeur',
'Email Address': 'Adresse courriel',
'Enable': 'Activer',
'Enable Close-Tag': 'Enable Close-Tag',
'Enable Code Folding': 'Enable Code Folding',
'Enterprise Web Framework': 'Enterprise Web Framework',
'Error': 'Error',
'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"',
'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket',
'Errors': 'erreurs',
'Error ticket': "Billet d'erreur",
'Errors': 'Erreurs',
'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s',
'Exception %s': 'Exception %s',
'Exception instance attributes': "Attributs d'instance Exception",
'Exit Fullscreen': 'Exit Fullscreen',
'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)',
'export as csv file': 'export au format CSV',
'Exports:': 'Exports:',
'Exports:': 'Exportions:',
'exposes': 'expose',
'exposes:': 'expose:',
'extends': 'étend',
'failed to compile file because:': 'failed to compile file because:',
'failed to reload module': 'impossible de recharger le module',
'failed to reload module because:': 'impossible de recharger le module car:',
'File': 'File',
'File': 'Fichier',
'file "%(filename)s" created': 'fichier "%(filename)s" créé',
'file "%(filename)s" deleted': 'fichier "%(filename)s" supprimé',
'file "%(filename)s" uploaded': 'fichier "%(filename)s" chargé',
@@ -247,19 +248,19 @@
'file not found': 'file not found',
'file saved on %(time)s': 'fichier enregistré le %(time)s',
'file saved on %s': 'fichier enregistré le %s',
'filename': 'filename',
'Filename': 'Filename',
'Files added': 'Files added',
'filename': 'nom de fichier',
'Filename': 'Nom de fichier',
'Files added': 'Fichiers ajoutés',
'filter': 'filtre',
'Find Next': 'Find Next',
'Find Previous': 'Find Previous',
'Form has errors': 'Form has errors',
'Find Next': 'Trouver les suivants',
'Find Previous': 'Trouver les précédents',
'Form has errors': 'Le formulaire comporte des erreurs',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraîneront des tests [passed] .',
'GAE Email': 'GAE Email',
'GAE Output': 'GAE Output',
'GAE Password': 'GAE Password',
'Generate': 'Generate',
'GAE Password': 'Mot de passe GAE',
'Generate': 'Générer',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Globals##debug': 'Globals##debug',
@@ -267,15 +268,15 @@
'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface',
'Google Application Id': 'Google Application Id',
'Goto': 'Goto',
'graph model': 'graph model',
'Graph Model': 'Graph Model',
'graph model': 'représentation graphique du modèle',
'Graph Model': 'Représentation graphique du modèle',
'Help': 'aide',
'here': 'here',
'here': 'ici',
'Hide/Show Translated strings': 'Hide/Show Translated strings',
'Highlight current line': 'Highlight current line',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Hits': 'Hits',
'Home': 'Home',
'Home': 'Accueil',
'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true',
'htmledit': 'edition html',
'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback',
@@ -283,7 +284,7 @@
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': "Si le rapport ci-dessus contient un numéro de ticket, cela indique une défaillance dans l'exécution du contrôleur, avant toute tentative d'exécuter les doctests. Cela est généralement dû à une erreur d'indentation ou une erreur à l'extérieur du code de la fonction.\r\nUn titre vert indique que tous les tests (si définis) sont passés. Dans ce cas, les résultats des essais ne sont pas affichées.",
'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.',
'import': 'import',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.',
'includes': 'inclus',
@@ -311,14 +312,14 @@
'Invalid request': 'Invalid request',
'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)',
'invalid ticket': 'ticket non valide',
'Key': 'Key',
'Key': 'Clé',
'Keyboard shortcuts': 'Keyboard shortcuts',
'kill process': 'kill process',
'language file "%(filename)s" created/updated': 'fichier de langue "%(filename)s" créé/mis à jour',
'Language files (static strings) updated': 'Fichiers de langue (chaînes statiques) mis à jour ',
'languages': 'langues',
'Languages': 'Langues',
'Last Revision': 'Last Revision',
'Last Revision': 'Dernière révision',
'Last saved on:': 'Dernière sauvegarde le:',
'License for': 'Licence pour',
'License:': 'License:',
@@ -326,8 +327,8 @@
'Line number': 'Line number',
'lists by exception': 'lists by exception',
'lists by ticket': 'lists by ticket',
'Loading...': 'Loading...',
'loading...': 'Chargement ...',
'Loading...': 'Chargement...',
'loading...': 'chargement ...',
'Local Apps': 'Local Apps',
'locals': 'locals',
'Locals##debug': 'Locals##debug',
@@ -337,7 +338,7 @@
'Login to the Administrative Interface': "Se connecter à l'interface d'administration",
'Login/Register': 'Login/Register',
'Logout': 'déconnexion',
'lost password': 'lost password',
'lost password': 'mot de passe perdu',
'Main Menu': 'Main Menu',
'Manage': 'Manage',
'Manage %(action)s': 'Manage %(action)s',
@@ -350,7 +351,7 @@
'merge': 'fusionner',
'Models': 'Modèles',
'models': 'modèles',
'Modified On': 'Modified On',
'Modified On': 'Modifié le',
'Modules': 'Modules',
'modules': 'modules',
'Multi User Mode': 'Multi User Mode',
@@ -363,8 +364,8 @@
'New Record': 'Nouvelle Entrée',
'new record inserted': 'nouvelle entrée insérée',
'New simple application': 'Nouvelle application simple',
'next': 'next',
'next %s rows': 'next %s rows',
'next': 'suivant',
'next %s rows': '%s lignes suivantes',
'next 100 rows': '100 lignes suivantes',
'NO': 'NON',
'no changes': 'no changes',
@@ -374,8 +375,8 @@
'no package selected': 'no package selected',
'no permission to uninstall "%s"': 'no permission to uninstall "%s"',
'Node:': 'Node:',
'Not Authorized': 'Not Authorized',
'Not supported': 'Not supported',
'Not Authorized': 'Pas autori',
'Not supported': 'Pas supporté',
'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.',
'Number of entries: **%s**': 'Number of entries: **%s**',
"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.",
@@ -402,14 +403,14 @@
'Peeking at file': 'Jeter un oeil au fichier',
'Permission': 'Permission',
'Permissions': 'Permissions',
'Please': 'Please',
'Please': 'SVP',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" supprimé',
'Plugin "%s" in application': 'Plugin "%s" dans l\'application',
'plugin not specified': 'plugin not specified',
'Plugin page': 'Plugin page',
'plugins': 'plugins',
'Plugins': 'Plugins',
'plugins': 'plugiciels',
'Plugins': 'Plugiciels',
'Plural Form #%s': 'Plural Form #%s',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Propulsé par',
@@ -423,7 +424,7 @@
'Pull': 'Pull',
'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.',
'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.',
'Push': 'Push',
'Push': 'Pousser',
'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.',
'pygraphviz library not found': 'pygraphviz library not found',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
@@ -433,9 +434,9 @@
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Rapid Search': 'Rapid Search',
'Record': 'Record',
'record': 'entrée',
'Rapid Search': 'Recherche rapide',
'Record': 'Enregistrement',
'record': 'enregistrement',
'record does not exist': "l'entrée n'existe pas",
'record id': 'id entrée',
'Record id': 'Record id',
@@ -444,24 +445,24 @@
'Reload routes': 'Reload routes',
'Remove compiled': 'retirer compilé',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Replace',
'Replace All': 'Replace All',
'Replace': 'Remplacer',
'Replace All': 'Tout remplacer',
'Repository (%s)': 'Repository (%s)',
'request': 'request',
'requires distutils, but not installed': 'requires distutils, but not installed',
'requires python-git, but not installed': 'requires python-git, but not installed',
'Resolve Conflict file': 'Résoudre les conflits de fichiers',
'response': 'response',
'restart': 'restart',
'restart': 'redémarrer',
'restore': 'restaurer',
'return': 'return',
'Revert': 'Revert',
'revert': 'revenir',
'return': 'retour',
'Revert': "Revenir vers l'arrière",
'revert': "revenir vers l'arrière",
'reverted to revision %s': 'reverted to revision %s',
'Revision %s': 'Revision %s',
'Revision:': 'Revision:',
'Role': 'Role',
'Roles': 'Roles',
'Revision %s': 'Révision %s',
'Revision:': 'Révision:',
'Role': 'Rôle',
'Roles': 'Rôles',
'Rows in Table': 'Rows in Table',
'Rows in table': 'Lignes de la table',
'Rows selected': 'Lignes sélectionnées',
@@ -470,15 +471,15 @@
'Run tests in this file': 'Run tests in this file',
"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Lancer les tests dans ce fichier (pour lancer tous les fichiers, vous pouvez également utiliser le bouton nommé 'test')",
'Running on %s': 'Running on %s',
'Save': 'Enregistrer',
'save': 'sauver',
'Save file:': 'Save file:',
'Save file: %s': 'Save file: %s',
'Save model as...': 'Save model as...',
'Save via Ajax': 'Save via Ajax',
'Save': 'Sauvegarder',
'save': 'sauvegarder',
'Save file:': 'Sauvegarder le fichier:',
'Save file: %s': 'Sauvegarder le fichier : %s',
'Save model as...': 'Sauvegarder le modèle sous...',
'Save via Ajax': 'Sauvegarder via Ajax',
'Saved file hash:': 'Hash du Fichier enregistré:',
'Screenshot %s': 'Screenshot %s',
'Search': 'Search',
'Screenshot %s': "Capture d'écran %s",
'Search': 'Rechercher',
'Searching: **%s** %%{file}': 'Searching: **%s** %%{file}',
'Select Files to Package': 'Select Files to Package',
'selected': 'sélectionnés',
@@ -491,34 +492,34 @@
'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found',
'Singular Form': 'Singular Form',
'Site': 'Site',
'Size of cache:': 'Size of cache:',
'Size of cache:': 'Taille de la mémoire cache:',
'skip to generate': 'skip to generate',
'some files could not be removed': 'certains fichiers ne peuvent pas être supprimés',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed',
'source : db': 'source : db',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Start a new app',
'Start searching': 'Start searching',
'source : filesystem': 'source : système de fichier',
'Start a new app': 'Commencer une nouvelle application',
'Start searching': 'Débuté la recherche',
'Start wizard': "Démarrer l'assistant",
'state': 'état',
'Static': 'Static',
'static': 'statiques',
'Static': 'Statique',
'static': 'statique',
'Static files': 'Fichiers statiques',
'Statistics': 'Statistics',
'Step': 'Step',
'step': 'step',
'stop': 'stop',
'stop': 'arrêt',
'submit': 'envoyer',
'Submit': 'Submit',
'successful': 'successful',
'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet? ',
'switch to : db': 'switch to : db',
'Submit': 'Envoyer',
'successful': 'réussi',
'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet?',
'switch to : db': 'transférer dans : db',
'switch to : filesystem': 'switch to : filesystem',
'Tab width (# characters)': 'Tab width (# characters)',
'table': 'table',
'Table': 'Table',
'Temporary': 'Temporary',
'Temporary': 'Temporaire',
'test': 'tester',
'Testing application': "Test de l'application",
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "requête" est une condition comme "db.table1.field1==\'value\'". Quelque chose comme "db.table1.field1==db.table2.field2" aboutit à un JOIN SQL.',
@@ -530,14 +531,14 @@
'The data representation, define database tables and sets': 'La représentation des données, définir les tables et ensembles de la base de données',
'The presentations layer, views are also known as templates': 'Les couches de présentation, les vues sont également appelées modples',
'the presentations layer, views are also known as templates': 'la couche de présentation, les vues sont également appelées modèles',
'Theme': 'Theme',
'Theme': 'Thème',
'There are no controllers': "Il n'y a pas de contrôleurs",
'There are no models': "Il n'y a pas de modèles",
'There are no modules': "Il n'y a pas de modules",
'There are no plugins': "Il n'y a pas de plugins",
'There are no private files': 'There are no private files',
'There are no private files': "Il n'y a pas de fichiers privés",
'There are no static files': "Il n'y a pas de fichiers statiques",
'There are no translators': 'There are no translators',
'There are no translators': "Il n'y a pas de traducteurs",
'There are no translators, only default language is supported': "Il n'y a pas de traducteurs, seule la langue par défaut est prise en charge",
'There are no views': "Il n'y a pas de vues",
'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app',
@@ -552,9 +553,9 @@
'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.',
'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?',
'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".',
'Ticket': 'Ticket',
'Ticket ID': 'Ticket ID',
'Ticket Missing': 'Ticket Missing',
'Ticket': 'Billet',
'Ticket ID': 'Identifiant du Billet',
'Ticket Missing': 'Billet manquant',
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'TM': 'MD',
'to previous version.': 'à la version précédente.',
@@ -569,8 +570,8 @@
'Translation strings for the application': "Chaînes de traduction pour l'application",
'try': 'essayer',
'try something like': 'essayez quelque chose comme',
'Try the mobile interface': 'Try the mobile interface',
'try view': 'try view',
'Try the mobile interface': "Essayer l'interface pour mobile",
'try view': 'essayer la vue',
'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.',
'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.',
'Unable to check for upgrades': 'Impossible de vérifier les mises à jour',
@@ -595,25 +596,25 @@
'update': 'mettre à jour',
'update all languages': 'mettre à jour toutes les langues',
'Update:': 'Mise à jour:',
'Upgrade': 'Upgrade',
'Upgrade': 'Mese à jour de version',
'upgrade now': 'mettre à jour maintenant',
'upgrade now to %s': 'upgrade now to %s',
'upgrade now to %s': 'mettre à jour maintenant à %s',
'upgrade web2py now': 'mettre à jour web2py maintenant',
'upload': 'charger',
'Upload': 'Upload',
'upload': 'téléversé',
'Upload': 'Téléversé',
'Upload & install packed application': "Charger & installer l'application empaquetée",
'Upload a package:': 'Charger un paquet:',
'Upload and install packed application': 'Upload and install packed application',
'upload application:': "charger l'application:",
'Upload existing application': 'Charger une application existante',
'upload file:': 'charger le fichier:',
'Upload a package:': 'Téléverser un paquet:',
'Upload and install packed application': 'Téléversement et installation du paquet applicatif',
'upload application:': "téléverser l'application:",
'Upload existing application': 'Téléverser une application existante',
'upload file:': 'téléverser le fichier:',
'upload plugin file:': 'charger fichier plugin:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilisez (...)&(...) pour AND, (...)|(...) pour OR, et ~(...) pour NOT afin de construire des requêtes plus complexes. ',
'Use an url:': 'Utiliser une url:',
'user': 'utilisateur',
'User': 'User',
'Username': 'Username',
'Users': 'Users',
'User': 'Utilisateur',
'Username': "Nom d'utilisateur",
'Users': 'Utilisateurs',
'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.',
'variables': 'variables',
'Version': 'Version',
@@ -625,9 +626,9 @@
'Warning!': 'Warning!',
'WARNING:': 'WARNING:',
'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:',
'Web Framework': 'Framework Web',
'web2py Admin Password': 'web2py Admin Password',
'web2py apps to deploy': 'web2py apps to deploy',
'Web Framework': 'Cadre Web',
'web2py Admin Password': 'Mot de passe admin web2py',
'web2py apps to deploy': 'applications web2py à deployer',
'web2py Debugger': 'web2py Debugger',
'web2py downgrade': 'web2py downgrade',
'web2py is up to date': 'web2py est à jour',
@@ -635,10 +636,10 @@
'web2py Recent Tweets': 'Tweets récents sur web2py ',
'web2py upgrade': 'web2py upgrade',
'web2py upgraded; please restart it': 'web2py mis à jour; veuillez le redémarrer',
'Working...': 'Working...',
'Working...': 'Travail en cours...',
'WSGI reference name': 'WSGI reference name',
'YES': 'OUI',
'Yes': 'Yes',
'Yes': 'Oui',
'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button',
'You can inspect variables using the console below': 'You can inspect variables using the console below',
'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out',
+1 -1
View File
@@ -52,7 +52,7 @@ def verify_password(password):
if DEMO_MODE:
ret = True
elif not _config.get('password'):
ret - False
ret = False
elif _config['password'].startswith('pam_user:'):
session.pam_user = _config['password'][9:].strip()
import gluon.contrib.pam
@@ -0,0 +1,33 @@
.node {fill: steelblue;
stroke: #636363;
stroke-width: 1px;}
.auth {fill: lightgrey;}
.table {r: 10;}
.link {stroke: #bbbbbb;
stroke-width: 2px;}
td {padding: 4px;}
div.tooltip {
position: absolute;
text-align: left;
/* width: 140px; */
/* height: 28px;*/
padding: 0px 5px 0px 5px;
padding-top: 0px;
font: 12px sans-serif;
background: #fff7bc;
border: solid 1px #aaa;
border-radius: 6px;
pointer-events: none;}
h5 { font: 14px sans-serif;
background : #ec7014;
color: #ffffe5;
padding: 5px 2px 5px 2px;
margin-top: 1px;}
path {
fill: #aaaaaa;}
File diff suppressed because one or more lines are too long
+181
View File
@@ -0,0 +1,181 @@
function d3_graph() {
// Some reference links:
// How to get link ids instead of index
// http://stackoverflow.com/questions/23986466/d3-force-layout-linking-nodes-by-name-instead-of-index
// embedding web2py in d3
// http://stackoverflow.com/questions/34326343/embedding-d3-js-graph-in-a-web2py-bootstrap-page
// nodes and links are defined in appadmin.html <script>
var edges = [];
links.forEach(function(e) {
var sourceNode = nodes.filter(function(n) {
return n.name === e.source;
})[0],
targetNode = nodes.filter(function(n) {
return n.name === e.target;
})[0];
edges.push({
source: sourceNode,
target: targetNode,
value: 1});
});
edges.forEach(function(e) {
if (!e.source["linkcount"]) e.source["linkcount"] = 0;
if (!e.target["linkcount"]) e.target["linkcount"] = 0;
e.source["linkcount"]++;
e.target["linkcount"]++;
});
//var width = 960, height = 600;
var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight;
var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth;
var svg = d3.select("#vis").append("svg")
.attr("width", width)
.attr("height", height);
// updated for d3 v4.
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(strength))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide(35));
// Node charge strength. Repel strength greater for less links.
//function strength(d) { return -50/d["linkcount"] ; }
function strength(d) { return -25 ; }
// Link distance. Distance increases with number of links at source and target
function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; }
// Link strength. Strength is less for highly connected nodes (move towards target dist)
function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; }
simulation
.nodes(nodes)
.on("tick", tick);
simulation.force("link")
.links(edges)
.distance(distance)
.strength(strengthl);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25) // Moves the arrow head out, allow for radius
.attr("refY", 0) // -1.5
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll('.link')
.data(edges)
.enter().append('line')
.attr("class", "link")
.attr("marker-end", "url(#end)");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", function(d) { return "node " + d.type;})
.attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")"})
.classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);});
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// add the nodes
node.append('circle')
.attr('r', 16)
;
// add text
node.append("text")
.attr("x", 12)
.attr("dy", "-1.1em")
.text(function(d) {return d.name;});
node.on("mouseover", function(d) {
var g = d3.select(this); // the node (table)
// tooltip
var fields = d.fields;
var fieldformat = "<TABLE>";
fields.forEach(function(d) {
fieldformat += "<TR><TD><B>"+ d.name+"</B></TD><TD>"+ d.type+"</TD><TD>"+ d.disp+"</TD></TR>";
});
fieldformat += "</TABLE>";
var tiplength = d.fields.length;
// Define 'div' for tooltips
var div = d3.select("body").append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0)
.html('<h5>' + d.name + '</h5>' + fieldformat)
.style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px")
.style("top", tooltop(tiplength))// or ...
.transition()
.duration(800)
.style("opacity", 0.9);
});
function tooltop(tiplength) {
//aim to ensure tooltip is fully visible whenver possible
return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px"
}
node.on("mouseout", function(d) {
d3.select("body").select('div.tooltip').remove();
});
// instead of waiting for force to end with : force.on('end', function()
// use .on("tick", instead. Here is the tick function
function tick() {
node.attr('transform', function(d) {
d.x = Math.max(30, Math.min(width - 16, d.x));
d.y = Math.max(30, Math.min(height - 16, d.y));
return "translate(" + d.x + "," + d.y + ")"; });
link.attr('x1', function(d) {return d.source.x;})
.attr('y1', function(d) {return d.source.y;})
.attr('x2', function(d) {return d.target.x;})
.attr('y2', function(d) {return d.target.y;});
};
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
};
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};
};
File diff suppressed because one or more lines are too long
+40 -10
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script>
{{include 'web2py_ajax.html'}}
+3 -1
View File
@@ -105,7 +105,9 @@ def deletefile(arglist, vars={}):
{{if os.access(os.path.join(request.folder,'..',app,'databases','sql.log'),os.R_OK):}}
{{=button(URL('peek/%s/databases/sql.log'%app), 'sql.log')}}
{{pass}}
{{=button(URL(a=app, c='appadmin',f='graph_model'), T('graph model'))}}
{{if os.access(os.path.join(request.folder,'..','admin','static','js','d3_graph.js'),os.R_OK):}}
{{=button(URL(a=app, c='appadmin',f='d3_graph_model'), T('graph model'))}}
{{pass}}
</div>
<ul class="unstyled act_edit">
{{for m in models:}}
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script>
{{include 'web2py_ajax.html'}}
+49 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -565,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -700,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
response.files.append(URL('admin','static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
File diff suppressed because one or more lines are too long
+40 -10
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
+1 -1
View File
@@ -1 +1 @@
{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
+49 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -565,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -700,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
response.files.append(URL('admin','static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
+73 -61
View File
@@ -3,8 +3,8 @@
'!langcode!': 'fr-ca',
'!langname!': 'Français (Canadien)',
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" est une expression optionnelle comme "champ1=\'nouvellevaleur\'". Vous ne pouvez mettre à jour ou supprimer les résultats d\'un JOIN',
'%s %%{row} deleted': '%s rangées supprimées',
'%s %%{row} updated': '%s rangées mises à jour',
'%s %%{row} deleted': '%s lignes supprimées',
'%s %%{row} updated': '%s lignes mises à jour',
'%s selected': '%s sélectionné',
'%Y-%m-%d': '%Y-%m-%d',
'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S',
@@ -13,12 +13,13 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'about': 'à propos',
'About': 'À propos',
'Access Control': "Contrôle d'accès",
'admin': 'admin',
'Administrative Interface': 'Administrative Interface',
'Administrative Interface': "Interface d'administration",
'Administrative interface': "Interface d'administration",
'Ajax Recipes': 'Recettes Ajax',
'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
@@ -37,37 +38,39 @@
'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer',
'Check to delete:': 'Cliquez pour supprimer:',
'Clear CACHE?': 'Clear CACHE?',
'Clear DISK': 'Clear DISK',
'Clear RAM': 'Clear RAM',
'Clear CACHE?': 'Vider le CACHE?',
'Clear DISK': 'Vider le DISQUE',
'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client',
'Community': 'Communauté',
'Components and Plugins': 'Components and Plugins',
'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini',
'Controller': 'Contrôleur',
'Copyright': "Droit d'auteur",
'Created By': 'Créé par',
'Created On': 'Créé le',
'Current request': 'Demande actuelle',
'Current response': 'Réponse actuelle',
'Current session': 'Session en cours',
'customize me!': 'personnalisez-moi!',
'data uploaded': 'données téléchargées',
'Database': 'base de données',
'Database %s select': 'base de données %s select',
'Database %s select': 'base de données %s selectionnée',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'db',
'DB Model': 'Modèle DB',
'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:',
'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement ',
'Description': 'Descriptif',
'Deployment Recipes': 'Recettes de déploiement',
'Description': 'Description',
'design': 'design',
'Design': 'Design',
'DISK': 'DISK',
'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
'Disk Cleared': 'Disque vidé',
'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Documentation': 'Documentation',
"Don't know what to do?": "Don't know what to do?",
"Don't know what to do?": 'Vous ne savez pas quoi faire?',
'done!': 'fait!',
'Download': 'Téléchargement',
'E-mail': 'Courriel',
@@ -75,26 +78,27 @@
'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application',
'Email and SMS': 'Email and SMS',
'Email and SMS': 'Courriel et texto',
'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
'enter an integer between %(min)g and %(max)g': 'entrer un entier compris entre %(min)g et %(max)g',
'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv',
'FAQ': 'faq',
'FAQ': 'FAQ',
'First name': 'Prénom',
'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée',
'Graph Model': 'Graph Model',
'Graph Model': 'Représentation graphique du modèle',
'Group %(group_id)s created': '%(group_id)s groupe créé',
'Group ID': 'Groupe ID',
'Group ID': 'ID du groupe',
'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s",
'Groups': 'Groupes',
'Hello World': 'Bonjour le monde',
'Helping web2py': 'Helping web2py',
'Helping web2py': 'Aider web2py',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Home': 'Accueil',
'How did you get here?': 'How did you get here?',
'import': 'import',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'Index': 'Index',
'insert new': 'insérer un nouveau',
@@ -104,40 +108,47 @@
'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide',
'Key': 'Key',
'Is Active': 'Est actif',
'Key': 'Clé',
'Last name': 'Nom',
'Layout': 'Mise en page',
'Layout Plugins': 'Layout Plugins',
'Layouts': 'layouts',
'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'Mises en page',
'Live chat': 'Clavardage en direct',
'Live Chat': 'Live Chat',
'Log In': 'Log In',
'Live Chat': 'Clavardage en direct',
'Loading...': 'Chargement...',
'loading...': 'chargement...',
'Log In': 'Connexion',
'Logged in': 'Connecté',
'login': 'connectez-vous',
'Login': 'Connectez-vous',
'logout': 'déconnectez-vous',
'login': 'connexion',
'Login': 'Connexion',
'logout': 'déconnexion',
'lost password': 'mot de passe perdu',
'Lost Password': 'Mot de passe perdu',
'Lost password?': 'Mot de passe perdu?',
'lost password?': 'mot de passe perdu?',
'Main Menu': 'Menu principal',
'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control',
'Manage Cache': 'Manage Cache',
'Manage Cache': 'Gérer le Cache',
'Memberships': 'Memberships',
'Menu Model': 'Menu modèle',
'My Sites': 'My Sites',
'Modified By': 'Modifié par',
'Modified On': 'Modifié le',
'My Sites': 'Mes sites',
'Name': 'Nom',
'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré',
'next %s rows': 'next %s rows',
'next %s rows': '%s prochaine lignes',
'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**',
'Object or table name': 'Objet ou nom de table',
'Online book': 'Online book',
'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine',
'Other Plugins': 'Other Plugins',
'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes',
'Overview': 'Présentation',
'password': 'mot de passe',
@@ -145,33 +156,34 @@
"Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission',
'Permissions': 'Permissions',
'please input your password again': "S'il vous plaît entrer votre mot de passe",
'please input your password again': "S'il vous plaît entrer votre mot de passe à nouveau",
'Plugins': 'Plugiciels',
'Powered by': 'Alimenté par',
'Preface': 'Préface',
'previous %s rows': 'previous %s rows',
'previous %s rows': '%s lignes précédentes',
'previous 100 rows': '100 lignes précédentes',
'profile': 'profile',
'pygraphviz library not found': 'pygraphviz library not found',
'profile': 'profil',
'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python',
'Query:': 'Requête:',
'Quick Examples': 'Examples Rapides',
'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
'Ram Cleared': 'Ram vidée',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Readme': 'Lisez-moi',
'Recipes': 'Recettes',
'Record': 'enregistrement',
'Record %(id)s created': 'Record %(id)s created',
'Record %(id)s updated': 'Record %(id)s updated',
'Record Created': 'Record Created',
'Record %(id)s created': 'Enregistrement %(id)s créé',
'Record %(id)s updated': 'Enregistrement %(id)s modifié',
'Record Created': 'Enregistrement créé',
'record does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement",
'Record id': "id d'enregistrement",
'Record Updated': 'Record Updated',
'Record ID': "ID de l'enregistrement",
'Record id': "id de l'enregistrement",
'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire",
'register': "s'inscrire",
'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
@@ -179,18 +191,18 @@
'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources',
'Role': 'Rôle',
'Roles': 'Roles',
'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Save model as...',
'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique',
'Services': 'Services',
'Sign Up': 'Sign Up',
'Size of cache:': 'Size of cache:',
'Sign Up': "S'inscrire",
'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état',
'Statistics': 'Statistics',
'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style',
'submit': 'submit',
'submit': 'soumettre',
'Submit': 'Soumettre',
'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
@@ -202,31 +214,31 @@
'The Views': 'Les Vues',
'This App': 'Cette Appli',
'This is a copy of the scaffolding application': "Ceci est une copie de l'application échafaudage",
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'Time in Cache (h:m:s)': 'Temps en Cache (h:m:s)',
'Timestamp': 'Horodatage',
'Traceback': 'Traceback',
'Twitter': 'Twitter',
'unable to parse csv file': "incapable d'analyser le fichier cvs",
'Update:': 'Mise à jour:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT pour construire des requêtes plus complexes.',
'User': 'User',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT afin de construire des requêtes plus complexes.',
'User': 'Utilisateur',
'User %(id)s Logged-in': 'Utilisateur %(id)s connecté',
'User %(id)s Registered': 'Utilisateur %(id)s enregistré',
'User ID': 'ID utilisateur',
'User Voice': 'User Voice',
'Users': 'Users',
'value already in database or empty': 'valeur déjà dans la base ou vide',
'User Voice': "Voix de l'utilisateur",
'Users': 'Utilisateurs',
'value already in database or empty': 'valeur déjà dans la base ou inexistante',
'Verify Password': 'Vérifiez le mot de passe',
'Videos': 'Vidéos',
'View': 'Présentation',
'View': 'Vue',
'Web2py': 'Web2py',
'Welcome': 'Bienvenu',
'Welcome': 'Bienvenue',
'Welcome %s': 'Bienvenue %s',
'Welcome to web2py': 'Bienvenue à web2py',
'Welcome to web2py!': 'Welcome to web2py!',
'Welcome to web2py!': 'Bienvenue à web2py!',
'Which called the function %s located in the file %s': 'Qui a appelé la fonction %s se trouvant dans le fichier %s',
'Working...': 'Working...',
'You are successfully running web2py': 'Vous roulez avec succès web2py',
'Working...': 'Traitement en cours...',
'You are successfully running web2py': 'Vous exécutez avec succès web2py',
'You can modify this application and adapt it to your needs': "Vous pouvez modifier cette application et l'adapter à vos besoins",
'You visited the url %s': "Vous avez visité l'URL %s",
}
+48 -32
View File
@@ -13,7 +13,9 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'about': 'à propos',
'About': 'À propos',
'Access Control': "Contrôle d'accès",
'admin': 'admin',
@@ -31,7 +33,7 @@
'Cache': 'Cache',
'Cache Cleared': 'Cache Cleared',
'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Cache Keys': 'Clés de cache',
'Cache Keys': 'Cache Keys',
'Cannot be empty': 'Ne peut pas être vide',
'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer',
@@ -41,10 +43,10 @@
'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client',
'Community': 'Communauté',
'Components and Plugins': 'Composants et Plugins',
'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini',
'Controller': 'Contrôleur',
'Copyright': 'Copyright',
'Copyright': "Droit d'auteur",
'Created By': 'Créé par',
'Created On': 'Créé le',
'Current request': 'Demande actuelle',
@@ -55,8 +57,8 @@
'Database': 'base de données',
'Database %s select': 'base de données %s selectionnée',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'bdd',
'DB Model': 'Modèle BDD',
'db': 'db',
'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:',
'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement',
@@ -71,12 +73,13 @@
"Don't know what to do?": 'Vous ne savez pas quoi faire?',
'done!': 'fait!',
'Download': 'Téléchargement',
'E-mail': 'E-mail',
'E-mail': 'Courriel',
'Edit': 'Éditer',
'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application',
'Email and SMS': 'Email et SMS',
'Email and SMS': 'Courriel et texto',
'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
'enter an integer between %(min)g and %(max)g': 'entrez un entier entre %(min)g et %(max)g',
'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv',
@@ -85,22 +88,24 @@
'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée',
'Graph Model': 'Graph Model',
'Group ID': 'Groupe ID',
'Graph Model': 'Représentation graphique du modèle',
'Group %(group_id)s created': '%(group_id)s groupe créé',
'Group ID': 'ID du groupe',
'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s",
'Groups': 'Groupes',
'Hello World': 'Bonjour le monde',
'Helping web2py': 'En train daider web2py',
'Helping web2py': 'Aider web2py',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Home': 'Accueil',
'How did you get here?': 'Comment êtes-vous arrivé ici?',
'import': 'Importer',
'How did you get here?': 'How did you get here?',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'Index': 'Index',
'insert new': 'insérer un nouveau',
'insert new %s': 'insérer un nouveau %s',
'Internal State': 'État interne',
'Introduction': 'Introduction',
'Invalid email': 'E-mail invalide',
'Introduction': 'Présentation',
'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide',
'Is Active': 'Est actif',
@@ -109,12 +114,15 @@
'Layout': 'Mise en page',
'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'Mises en page',
'Live chat': 'Chat en direct',
'Live Chat': 'Chat en direct',
'Log In': 'Log In',
'login': 'connectez-vous',
'Login': 'Connectez-vous',
'logout': 'déconnectez-vous',
'Live chat': 'Clavardage en direct',
'Live Chat': 'Clavardage en direct',
'Loading...': 'Chargement...',
'loading...': 'chargement...',
'Log In': 'Connexion',
'Logged in': 'Connecté',
'login': 'connexion',
'Login': 'Connexion',
'logout': 'déconnexion',
'lost password': 'mot de passe perdu',
'Lost Password': 'Mot de passe perdu',
'Lost password?': 'Mot de passe perdu?',
@@ -131,7 +139,7 @@
'Name': 'Nom',
'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré',
'next %s rows': 'next %s rows',
'next %s rows': '%s prochaine lignes',
'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**',
@@ -140,55 +148,63 @@
'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine',
'Other Plugins': 'Autres Plugins',
'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes',
'Overview': 'Présentation',
'password': 'mot de passe',
'Password': 'Mot de passe',
"Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission',
'Permissions': 'Permissions',
'Plugins': 'Plugins',
'please input your password again': "S'il vous plaît entrer votre mot de passe à nouveau",
'Plugins': 'Plugiciels',
'Powered by': 'Alimenté par',
'Preface': 'Préface',
'previous %s rows': 'previous %s rows',
'previous %s rows': '%s lignes précédentes',
'previous 100 rows': '100 lignes précédentes',
'profile': 'profil',
'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python',
'Query:': 'Requête:',
'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM',
'RAM Cache Keys': 'Clés de cache de la RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram vidée',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Readme': 'Lisez-moi',
'Recipes': 'Recettes',
'Record': 'enregistrement',
'Record %(id)s created': 'Enregistrement %(id)s créé',
'Record %(id)s updated': 'Enregistrement %(id)s modifié',
'Record Created': 'Enregistrement créé',
'record does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement",
'Record id': "id d'enregistrement",
'Record ID': "ID de l'enregistrement",
'Record id': "id de l'enregistrement",
'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire",
'register': "s'inscrire",
'Registration identifier': "Identifiant d'enregistrement",
'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
'Request reset password': 'Demande de réinitialiser le mot clé',
'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources',
'Role': 'Rôle',
'Roles': 'Roles',
'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique',
'Services': 'Services',
'Sign Up': 'Sign Up',
'Size of cache:': 'Taille du cache:',
'Sign Up': "S'inscrire",
'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état',
'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style',
'submit': 'soumettre',
'Submit': 'Soumettre',
'Support': 'Support',
'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
'Table': 'tableau',
'Table name': 'Nom du tableau',
File diff suppressed because one or more lines are too long
+40 -10
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
+1 -1
View File
@@ -27,4 +27,4 @@ Notice:
- no need to return a string
even if the function is called via ajax.
'''}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
'''}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
+1 -1
View File
@@ -59,7 +59,7 @@
</button>
{{=response.logo or ''}}
</div>
<div class="collapse navbar-collapse navbar-ex1-collapse">
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
{{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}}
</ul>
+1038
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -637,7 +637,7 @@ def run_controller_in(controller, function, environment):
web2py_error=badf)
code = "%s\nresponse._vars=response._caller(%s)" % (code, function)
layer = "%s:%s" % (filename, function)
ccode = getcfs(layer, filename, lambda: compile2(code, layer))
ccode = getcfs(layer, filename, lambda: compile2(code, filename))
restricted(ccode, environment, layer=filename)
response = environment["response"]
+4 -4
View File
@@ -506,9 +506,9 @@ def ldap_auth(server='ldap',
l = []
for group in ldap_groups_of_the_user:
if group in group_mapping:
l += group_mapping[group]
l.append(group_mapping[group])
ldap_groups_of_the_user = l
logging.info("User groups after remapping: %s" % str(l))
logger.info("User groups after remapping: %s" % str(l))
#
# Get all group name where the user is in actually in local db
@@ -528,7 +528,7 @@ def ldap_auth(server='ldap',
except AttributeError as e:
db_user_id = db.auth_user.insert(email=username, first_name=username)
if not db_user_id:
logging.error(
logger.error(
'There is no username or email for %s!' % username)
raise
# if old pydal version, assume this is a relational database which can do joins
@@ -550,7 +550,7 @@ def ldap_auth(server='ldap',
for group in db_group_search.select(db.auth_group.id, db.auth_group.role, distinct=True):
db_group_id[group.role] = group.id
db_groups_of_the_user.append(group.role)
logging.debug('db groups of user %s: %s' % (username, str(db_groups_of_the_user)))
logger.debug('db groups of user %s: %s' % (username, str(db_groups_of_the_user)))
auth_membership_changed = False
#
+1 -1
View File
@@ -544,7 +544,7 @@ regex_code = re.compile(
'(' + META + '|' + DISABLED_META + r'|````)|(``(?P<t>.+?)``(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[^\]]*)\])?)?)',
re.S)
regex_strong = re.compile(r'\*\*(?P<t>[^\s*]+( +[^\s*]+)*)\*\*')
regex_del = re.compile(r'~~(?P<t>[^\s*]+( +[^\s*]+)*)~~')
regex_del = re.compile(r'~~(?P<t>[^\s~]+( +[^\s~]+)*)~~')
regex_em = re.compile(r"''(?P<t>([^\s']| |'(?!'))+)''")
regex_num = re.compile(r"^\s*[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\s*$")
regex_list = re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$')
+3
View File
@@ -21,6 +21,9 @@ def MemcacheClient(*a, **b):
class MemcacheClientObj(Client):
def initialize(self):
pass
meta_storage = {}
max_time_expire = 24*3600
+3 -3
View File
@@ -105,14 +105,14 @@ class ServerProxy(object):
def __getattr__(self, attr):
"pseudo method that can be called"
return lambda *args: self.call(attr, *args)
return lambda *args, **vars: self.call(attr, *args, **vars)
def call(self, method, *args):
def call(self, method, *args, **vars):
"JSON RPC communication (method invocation)"
# build data sent to the service
request_id = random.randint(0, sys.maxsize)
data = {'id': request_id, 'method': method, 'params': args, }
data = {'id': request_id, 'method': method, 'params': args or vars, }
if self.version:
data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600
request = json.dumps(data)
+2 -2
View File
@@ -43,6 +43,7 @@ class WebClient(object):
self.forms = {}
self.history = []
self.cookies = {}
self.cookiejar = cookielib.CookieJar()
self.default_headers = default_headers
self.sessions = {}
self.session_regex = session_regex and re.compile(session_regex)
@@ -79,9 +80,8 @@ class WebClient(object):
cookies = cookies or {}
headers = headers or {}
cj = cookielib.CookieJar()
args = [
urllib2.HTTPCookieProcessor(cj),
urllib2.HTTPCookieProcessor(self.cookiejar),
urllib2.HTTPHandler(debuglevel=0)
]
# if required do basic auth
+2 -2
View File
@@ -75,7 +75,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
try:
oname = name if not name.startswith('.') else '.'+name
return NATIVE_IMPORTER(oname, globals, locals, fromlist, level)
except ImportError:
except (ImportError, KeyError):
items = current.request.folder.split(os.path.sep)
if not items[-1]:
items = items[:-1]
@@ -100,7 +100,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
import_tb = sys.exc_info()[2]
try:
return NATIVE_IMPORTER(name, globals, locals, fromlist, level)
except ImportError as e3:
except (ImportError, KeyError) as e3:
raise ImportError(e1, import_tb) # there an import error in the module
except Exception as e2:
raise # there is an error in the module
+13 -13
View File
@@ -89,7 +89,7 @@ def communicate(command=None):
# New debugger implementation using dbg and a web UI
import gluon.contrib.dbg as dbg
import gluon.contrib.dbg as c_dbg
from threading import RLock
interact_lock = RLock()
@@ -109,11 +109,11 @@ def check_interaction(fn):
return check_fn
class WebDebugger(dbg.Frontend):
class WebDebugger(c_dbg.Frontend):
"""Qdb web2py interface"""
def __init__(self, pipe, completekey='tab', stdin=None, stdout=None):
dbg.Frontend.__init__(self, pipe)
c_dbg.Frontend.__init__(self, pipe)
self.clear_interaction()
def clear_interaction(self):
@@ -128,7 +128,7 @@ class WebDebugger(dbg.Frontend):
run_lock.acquire()
try:
while self.pipe.poll():
dbg.Frontend.run(self)
c_dbg.Frontend.run(self)
finally:
run_lock.release()
@@ -149,23 +149,23 @@ class WebDebugger(dbg.Frontend):
@check_interaction
def do_continue(self):
dbg.Frontend.do_continue(self)
c_dbg.Frontend.do_continue(self)
@check_interaction
def do_step(self):
dbg.Frontend.do_step(self)
c_dbg.Frontend.do_step(self)
@check_interaction
def do_return(self):
dbg.Frontend.do_return(self)
c_dbg.Frontend.do_return(self)
@check_interaction
def do_next(self):
dbg.Frontend.do_next(self)
c_dbg.Frontend.do_next(self)
@check_interaction
def do_quit(self):
dbg.Frontend.do_quit(self)
c_dbg.Frontend.do_quit(self)
def do_exec(self, statement):
interact_lock.acquire()
@@ -175,18 +175,18 @@ class WebDebugger(dbg.Frontend):
# avoid spurious interaction notifications:
self.set_burst(2)
# execute the statement in the remote debugger:
return dbg.Frontend.do_exec(self, statement)
return c_dbg.Frontend.do_exec(self, statement)
finally:
interact_lock.release()
# create the connection between threads:
parent_queue, child_queue = Queue.Queue(), Queue.Queue()
front_conn = dbg.QueuePipe("parent", parent_queue, child_queue)
child_conn = dbg.QueuePipe("child", child_queue, parent_queue)
front_conn = c_dbg.QueuePipe("parent", parent_queue, child_queue)
child_conn = c_dbg.QueuePipe("child", child_queue, parent_queue)
web_debugger = WebDebugger(front_conn) # frontend
dbg_debugger = dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend
dbg_debugger = c_dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend
dbg = dbg_debugger
# enable getting context (stack, globals/locals) at interaction
+2 -2
View File
@@ -9,7 +9,7 @@ Based on http://code.activestate.com/recipes/52257/
Licensed under the PSF License
"""
from gluon._compat import to_unicode
import codecs
# None represents a potentially variable byte. "##" in the XML spec...
@@ -77,4 +77,4 @@ def autoDetectXMLEncoding(buffer):
def decoder(buffer):
encoding = autoDetectXMLEncoding(buffer)
return buffer.decode(encoding).encode('utf8')
return to_unicode(buffer, charset=encoding)
+5 -5
View File
@@ -89,10 +89,11 @@ class SortingPickler(Pickler):
self._batch_setitems([(key, obj[key]) for key in sorted(obj)])
if PY2:
#FIXME PY3
SortingPickler.dispatch = copy.copy(Pickler.dispatch)
SortingPickler.dispatch[dict] = SortingPickler.save_dict
else:
SortingPickler.dispatch_table = copyreg.dispatch_table.copy()
SortingPickler.dispatch_table[dict] = SortingPickler.save_dict
def sorting_dumps(obj, protocol=None):
file = StringIO()
@@ -349,14 +350,14 @@ class Request(Storage):
current.session.forget()
redirect(URL(scheme='https', args=self.args, vars=self.vars))
def restful(self):
def restful(self, ignore_extension=False):
def wrapper(action, request=self):
def f(_action=action, *a, **b):
request.is_restful = True
env = request.env
is_json = env.content_type=='application/json'
method = env.request_method
if len(request.args) and '.' in request.args[-1]:
if not ignore_extension and len(request.args) and '.' in request.args[-1]:
request.args[-1], _, request.extension = request.args[-1].rpartition('.')
current.response.headers['Content-Type'] = \
contenttype('.' + request.extension.lower())
@@ -415,7 +416,6 @@ class Response(Storage):
if not escape:
self.body.write(str(data))
else:
# FIXME PY3:
self.body.write(to_native(xmlescape(data)))
def render(self, *a, **b):
+13 -27
View File
@@ -20,7 +20,7 @@ import urllib
import base64
from gluon import sanitizer, decoder
import itertools
from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type
from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type, long
from gluon.utils import local_html_escape
import marshal
@@ -596,10 +596,10 @@ class XML(XmlComponent):
for A, IMG and BlockQuote).
The key is the tag; the value is a list of allowed attributes.
"""
if sanitize:
text = sanitizer.sanitize(text, permitted_tags, allowed_attributes)
if isinstance(text, unicodeT):
text = to_native(text.encode('utf8', 'xmlcharrefreplace'))
if sanitize:
text = sanitizer.sanitize(text, permitted_tags, allowed_attributes)
elif isinstance(text, bytes):
text = to_native(text)
elif not isinstance(text, str):
@@ -998,9 +998,9 @@ class DIV(XmlComponent):
if isinstance(c, XmlComponent):
s = c.flatten(render)
elif render:
s = render(str(c))
s = render(to_native(c))
else:
s = str(c)
s = to_native(c)
text += s
if render:
text = render(text, self.tag, self.attributes)
@@ -1281,7 +1281,6 @@ class __TAG__(XmlComponent):
def __getattr__(self, name):
if name[-1:] == '_':
name = name[:-1] + '/'
name=to_bytes(name)
return lambda *a, **b: __tag_div__(name, *a, **b)
def __call__(self, html):
@@ -2376,17 +2375,17 @@ class FORM(DIV):
def as_json(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import json
from gluon.serializers import json
return json(d)
def as_yaml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import yaml
from gluon.serializers import yaml
return yaml(d)
def as_xml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import xml
from gluon.serializers import xml
return xml(d)
@@ -2655,36 +2654,24 @@ class web2pyHTMLParser(HTMLParser):
"""
obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers.
obj.tree contains the root of the tree, and tree can be manipulated
>>> str(web2pyHTMLParser('hello<div a="b" c=3>wor&lt;ld<span>xxx</span>y<script/>yy</div>zzz').tree)
'hello<div a="b" c="3">wor&lt;ld<span>xxx</span>y<script></script>yy</div>zzz'
>>> str(web2pyHTMLParser('<div>a<span>b</div>c').tree)
'<div>a<span>b</span></div>c'
>>> tree = web2pyHTMLParser('hello<div a="b">world</div>').tree
>>> tree.element(_a='b')['_c']=5
>>> str(tree)
'hello<div a="b" c="5">world</div>'
"""
def __init__(self, text, closed=('input', 'link')):
HTMLParser.__init__(self)
self.tree = self.parent = TAG['']()
self.closed = closed
self.tags = [x for x in __all__ if isinstance(eval(x), DIV)]
self.last = None
self.feed(text)
def handle_starttag(self, tagname, attrs):
if tagname.upper() in self.tags:
tag = eval(tagname.upper())
else:
if tagname in self.closed:
tagname += '/'
tag = TAG[tagname]()
if tagname in self.closed:
tagname += '/'
tag = TAG[tagname]()
for key, value in attrs:
tag['_' + key] = value
tag.parent = self.parent
self.parent.append(tag)
if not tag.tag.endswith(b'/'):
if not tag.tag.endswith('/'):
self.parent = tag
else:
self.last = tag.tag[:-1]
@@ -2707,7 +2694,6 @@ class web2pyHTMLParser(HTMLParser):
self.parent.append(entitydefs[name])
def handle_endtag(self, tagname):
tagname = to_bytes(tagname)
# this deals with unbalanced tags
if tagname == self.last:
return
+9 -6
View File
@@ -19,13 +19,14 @@ import sched
import re
import datetime
import platform
import gluon.fileutils
from functools import reduce
try:
import cPickle as pickle
except:
import pickle
from gluon.settings import global_settings
from gluon import fileutils
from gluon._compat import to_bytes
from pydal.contrib import portalocker
logger = logging.getLogger("web2py.cron")
@@ -116,7 +117,7 @@ class Token(object):
def __init__(self, path):
self.path = os.path.join(path, 'cron.master')
if not os.path.exists(self.path):
fileutils.write_file(self.path, '', 'wb')
fileutils.write_file(self.path, to_bytes(''), 'wb')
self.master = None
self.now = time.time()
@@ -139,7 +140,7 @@ class Token(object):
if portalocker.LOCK_EX is None:
logger.warning('WEB2PY CRON: Disabled because no file locking')
return None
self.master = open(self.path, 'rb+')
self.master = fileutils.open_file(self.path, 'rb+')
try:
ret = None
portalocker.lock(self.master, portalocker.LOCK_EX)
@@ -167,6 +168,7 @@ class Token(object):
"""
Writes into cron.master the time when cron job was completed
"""
ret = self.master.closed
if not self.master.closed:
portalocker.lock(self.master, portalocker.LOCK_EX)
logger.debug('WEB2PY CRON: Releasing cron lock')
@@ -177,6 +179,7 @@ class Token(object):
pickle.dump((self.now, time.time()), self.master)
portalocker.unlock(self.master)
self.master.close()
return ret
def rangetolist(s, period='min'):
@@ -222,8 +225,8 @@ def parsecronline(line):
params = line.strip().split(None, 6)
if len(params) < 7:
return None
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4,
'fri': 5, 'sat': 6}
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
'thu': 4, 'fri': 5, 'sat': 6}
for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
if not s in [None, '*']:
task[id] = []
@@ -236,7 +239,7 @@ def parsecronline(line):
elif val.isdigit() or val == '-1':
task[id].append(int(val))
elif id == 'dow' and val[:3].lower() in daysofweek:
task[id].append(daysofweek(val[:3].lower()))
task[id].append(daysofweek[val[:3].lower()])
task['user'] = params[5]
task['cmd'] = params[6]
return task
+4 -1
View File
@@ -137,7 +137,10 @@ class RestrictedError(Exception):
self.environment = environment
if layer:
try:
self.traceback = traceback.format_exc()
try:
self.traceback = traceback.format_exc()
except:
self.traceback = traceback.format_exc(limit=1)
except:
self.traceback = 'no traceback because template parsing error'
try:
+1 -1
View File
@@ -317,7 +317,7 @@ def load(routes='routes.py', app=None, data=None, rdict=None):
symbols = dict(app=app)
try:
exec(data + '\n', symbols)
exec(data, symbols)
except SyntaxError as e:
logger.error(
'%s has a syntax error and will not be loaded\n' % path
+4 -5
View File
@@ -307,13 +307,13 @@ try:
except ImportError:
has_futures = False
class Future:
class Future(object):
pass
class ThreadPoolExecutor:
class ThreadPoolExecutor(object):
pass
class _WorkItem:
class _WorkItem(object):
pass
@@ -784,8 +784,7 @@ class Rocket(object):
the application developer. Please update your \
applications to no longer call rocket.stop(True)"
try:
import warnings
raise warnings.DeprecationWarning(msg)
raise DeprecationWarning(msg)
except ImportError:
raise RuntimeError(msg)
+2 -2
View File
@@ -11,7 +11,7 @@ Cross-site scripting (XSS) defense
"""
from gluon._compat import HTMLParser, urlparse, entitydefs, basestring
from cgi import escape
from gluon.utils import local_html_escape
from formatter import AbstractFormatter
from xml.sax.saxutils import quoteattr
@@ -21,7 +21,7 @@ __all__ = ['sanitize']
def xssescape(text):
"""Gets rid of < and > and & and, for good measure, :"""
return escape(text, quote=True).replace(':', '&#58;')
return local_html_escape(text, quote=True).replace(':', '&#58;')
class XssCleaner(HTMLParser):
+2 -2
View File
@@ -1158,7 +1158,7 @@ class Scheduler(MetaScheduler):
if not self.db_thread:
logger.debug('thread building own DAL object')
self.db_thread = DAL(
self.db._uri, folder=self.db._adapter.folder)
self.db._uri, folder=self.db._adapter.folder, decode_credentials=True)
self.define_tables(self.db_thread, migrate=False)
try:
db = self.db_thread
@@ -1698,7 +1698,7 @@ def main():
print('groups for this worker: ' + ', '.join(group_names))
print('connecting to database in folder: ' + options.db_folder or './')
print('using URI: ' + options.db_uri)
db = DAL(options.db_uri, folder=options.db_folder)
db = DAL(options.db_uri, folder=options.db_folder, decode_credentials=True)
print('instantiating scheduler...')
scheduler = Scheduler(db=db,
worker_name=options.worker_name,
+30 -25
View File
@@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
from pydal.base import DEFAULT
from pydal.objects import Table, Row, Expression, Field, Set, Rows
from pydal.adapters.base import CALLABLETYPES
from pydal.helpers.methods import smart_query, bar_encode, _repr_ref
from pydal.helpers.methods import smart_query, bar_encode, _repr_ref, merge_tablemaps
from pydal.helpers.classes import Reference, SQLCustomType
from gluon.storage import Storage
from gluon.utils import md5_hash
@@ -2329,7 +2329,7 @@ class SQLFORM(FORM):
if not isinstance(left, (list, tuple)):
left = [left]
for join in left:
tablenames += db._adapter.tables(join)
tablenames = merge_tablemaps(tablenames, db._adapter.tables(join))
tables = [db[tablename] for tablename in tablenames]
if fields:
# add missing tablename to virtual fields
@@ -2341,10 +2341,11 @@ class SQLFORM(FORM):
else:
fields = []
columns = []
filter1 = lambda f: isinstance(f, Field) and f.readable and (f.type!='blob' or showblobs)
filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs)
filter2 = lambda f: isinstance(f, Field) and f.readable
for table in tables:
fields += filter(filter1, table)
columns += filter(filter1, table)
columns += filter(filter2, table)
for k, f in iteritems(table):
if not k.startswith('_'):
if isinstance(f, Field.Virtual) and f.readable:
@@ -2553,6 +2554,7 @@ class SQLFORM(FORM):
if isinstance(field, Field.Virtual) and not str(field) in expcolumns:
expcolumns.append(str(field))
expcolumns = ['"%s"' % '"."'.join(f.split('.')) for f in expcolumns]
if export_type in exportManager and exportManager[export_type]:
if keywords:
try:
@@ -3150,7 +3152,9 @@ class SQLFORM(FORM):
# if isinstance(linked_tables, dict):
# linked_tables = linked_tables.get(table._tablename, [])
if linked_tables is None or referee in linked_tables:
field.represent = lambda id, r=None, referee=referee, rep=field.represent: A(callable(rep) and rep(id) or id, cid=request.cid, _href=url(args=['view', referee, id]))
field.represent = (lambda id, r=None, referee=referee, rep=field.represent:
A(callable(rep) and rep(id) or id,
cid=request.cid, _href=url(args=['view', referee, id])))
except (KeyError, ValueError, TypeError):
redirect(URL(args=table._tablename))
if nargs == len(args) + 1:
@@ -3311,23 +3315,28 @@ class SQLTABLE(TABLE):
if not sqlrows:
return
REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD
fieldmap = dict(zip(sqlrows.colnames, sqlrows.fields))
tablemap = dict(((f.tablename, f.table) for f in fieldmap.values()))
for table in tablemap.values():
pref = table._tablename + '.'
fieldmap.update(((pref+f.name, f) for f in table._virtual_fields))
fieldmap.update(((pref+f.name, f) for f in table._virtual_methods))
field_types = (Field, Field.Virtual, Field.Method)
if not columns:
columns = list(sqlrows.colnames)
if headers == 'fieldname:capitalize':
header_func = {
'fieldname:capitalize': lambda f: f.name.replace('_', ' ').title(),
'labels': lambda f: f.label
}
if isinstance(headers, str) and headers in header_func:
make_name = header_func[headers]
headers = {}
for c in columns:
tfmatch = REGEX_TABLE_DOT_FIELD.match(c)
if tfmatch:
(t, f) = REGEX_TABLE_DOT_FIELD.match(c).groups()
headers[t + '.' + f] = f.replace('_', ' ').title()
f = fieldmap.get(c)
if isinstance(f, field_types):
headers[c] = make_name(f)
else:
headers[c] = REGEX_ALIAS_MATCH.sub(r'\2', c)
elif headers == 'labels':
headers = {}
for c in columns:
(t, f) = c.split('.')
field = sqlrows.db[t][f]
headers[c] = field.label
if colgroup:
cols = [COL(_id=c.replace('.', '-'), data={'column': i + 1})
for i, c in enumerate(columns)]
@@ -3380,9 +3389,8 @@ class SQLTABLE(TABLE):
_class += ' rowselected'
for colname in columns:
matched_column_field = \
sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD.match(colname)
if not matched_column_field:
field = fieldmap.get(colname)
if not isinstance(field, field_types):
if "_extra" in record and colname in record._extra:
r = record._extra[colname]
row.append(TD(r))
@@ -3390,12 +3398,9 @@ class SQLTABLE(TABLE):
else:
raise KeyError(
"Column %s not found (SQLTABLE)" % colname)
(tablename, fieldname) = matched_column_field.groups()
colname = tablename + '.' + fieldname
try:
field = sqlrows.db[tablename][fieldname]
except (KeyError, AttributeError):
field = None
# Virtual fields don't have parent table name...
tablename = colname.split('.', 1)[0]
fieldname = field.name
if tablename in record \
and isinstance(record, Row) \
and isinstance(record[tablename], Row):
+3 -1
View File
@@ -13,6 +13,7 @@ from .test_contribs import *
from .test_routes import *
from .test_router import *
from .test_validators import *
from .test_authapi import *
from .test_tools import *
from .test_utils import *
from .test_serializers import *
@@ -22,7 +23,8 @@ from .test_appadmin import *
from .test_web import *
from .test_sqlhtml import *
from .test_scheduler import *
from .test_cron import *
from .test_is_url import *
if sys.version[:3] == '2.7':
from .test_is_url import *
from .test_old_doctests import *
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Unit tests for authapi """
import os
import unittest
from gluon.globals import Request, Response, Session
from gluon.languages import translator
from gluon.dal import DAL, Field
from gluon.authapi import AuthAPI
from gluon.storage import Storage
from gluon._compat import to_bytes, to_native, add_charset
DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
class TestAuthAPI(unittest.TestCase):
def setUp(self):
self.request = Request(env={})
self.request.application = 'a'
self.request.controller = 'c'
self.request.function = 'f'
self.request.folder = 'applications/admin'
self.response = Response()
self.session = Session()
T = translator('', 'en')
self.session.connect(self.request, self.response)
from gluon.globals import current
self.current = current
self.current.request = self.request
self.current.response = self.response
self.current.session = self.session
self.current.T = T
self.db = DAL(DEFAULT_URI, check_reserved=['all'])
self.auth = AuthAPI(self.db)
self.auth.define_tables(username=True, signature=False)
# Create a user
self.auth.table_user().validate_and_insert(first_name='Bart',
last_name='Simpson',
username='bart',
email='bart@simpson.com',
password='bart_password',
registration_key='',
registration_id=''
)
self.db.commit()
def test_login(self):
result = self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.assertTrue(result['user']['email'] == 'bart@simpson.com')
self.auth.logout()
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.username_case_sensitive = False
result = self.auth.login(**{'username': 'BarT', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
def test_logout(self):
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.assertTrue(result['user'] is None)
def test_register(self):
self.auth.settings.login_after_registration = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['user']['email'] == 'lisa@simpson.com')
self.assertTrue(self.auth.is_logged_in())
with self.assertRaises(AssertionError): # Can't register if you're logged in
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.auth.logout()
self.auth.settings.login_after_registration = False
result = self.auth.register(**{
'username': 'barney',
'first_name': 'Barney',
'last_name': 'Gumble',
'email': 'barney@simpson.com',
'password': 'barney_password'
})
self.assertTrue(result['user']['email'] == 'barney@simpson.com')
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.login_userfield = 'email'
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['errors']['email'] == self.auth.messages.email_taken)
self.assertTrue(result['user'] is None)
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
def test_profile(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.profile()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.profile(email='bartolo@simpson.com')
self.assertTrue(result['user']['email'] == 'bartolo@simpson.com')
self.assertTrue(self.auth.table_user()[result['user']['id']].email == 'bartolo@simpson.com')
def test_change_password(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.change_password()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.auth.login(username='bart', password='1234')
self.assertTrue(self.auth.is_logged_in())
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='5678')
self.assertTrue('new_password2' in result['errors'])
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.assertTrue('old_password' in result['errors'])
def test_verify_key(self):
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
homer_id = result['user']['id']
homers_key = result['user']['key']
result = self.auth.verify_key(key=None)
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key='12345')
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key=homers_key)
self.assertTrue(result['errors'] is None)
self.assertEqual(self.auth.table_user()[homer_id].registration_key, '')
self.auth.settings.registration_requires_approval = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
lisa_id = result['user']['id']
result = self.auth.verify_key(key=result['user']['key'])
self.assertEqual(self.auth.table_user()[lisa_id].registration_key, 'pending')
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Unit tests for cron """
import unittest, os
from gluon.newcron import Token, crondance
class TestCron(unittest.TestCase):
def test_Token(self):
appname_path = os.path.join(os.getcwd(), 'applications', 'welcome')
t = Token(path=appname_path)
self.assertNotEqual(t.acquire(), None)
self.assertFalse(t.release())
self.assertEqual(t.acquire(), None)
self.assertTrue(t.release())
return
def test_crondance(self):
#TODO update crondance to return something
crondance(os.getcwd())
+72 -24
View File
@@ -10,27 +10,76 @@ import re
import unittest
from gluon.globals import Request, Response, Session
from gluon.rewrite import regex_url_in
from gluon import URL
from gluon._compat import basestring
def setup_clean_session():
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
session.connect(request, response)
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
session.connect(request, response)
from gluon.globals import current
current.request = request
current.response = response
current.session = session
return current
class testRequest(unittest.TestCase):
def setUp(self):
from gluon.globals import current
current.request = request
current.response = response
current.session = session
return current
current.response = Response()
def test_restful_simple(self):
env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/1.pdf'}
r = Request(env)
regex_url_in(r, env)
@r.restful()
def simple_rest():
def GET(*args, **vars):
return args[0]
return locals()
self.assertEqual(simple_rest(), '1')
def test_restful_calls_post(self):
env = {'request_method': 'POST', 'PATH_INFO': '/welcome/default/index'}
r = Request(env)
regex_url_in(r, env)
@r.restful()
def post_rest():
def POST(*args, **vars):
return 'I posted'
return locals()
self.assertEqual(post_rest(), 'I posted')
def test_restful_ignore_extension(self):
env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/127.0.0.1'}
r = Request(env)
regex_url_in(r, env)
@r.restful(ignore_extension=True)
def ignore_rest():
def GET(*args, **vars):
return args[0]
return locals()
self.assertEqual(ignore_rest(), '127.0.0.1')
class testResponse(unittest.TestCase):
#port from python 2.7, needed for 2.5 and 2.6 tests
# port from python 2.7, needed for 2.5 and 2.6 tests
def assertRegexpMatches(self, text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regexp, basestring):
@@ -99,8 +148,8 @@ class testResponse(unittest.TestCase):
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response)
self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
@@ -109,12 +158,11 @@ class testResponse(unittest.TestCase):
response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response)
self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
'<script src="/a/static/css/file.ts" type="text/typescript"></script>' +
'<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
)
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
'<script src="/a/static/css/file.ts" type="text/typescript"></script>' +
'<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
)
response = Response()
response.files.append(URL('a', 'static', 'css/file.js'))
@@ -122,13 +170,13 @@ class testResponse(unittest.TestCase):
content = return_includes(response, extensions=['css'])
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
#regr test for #628
# regr test for #628
response = Response()
response.files.append('http://maps.google.com/maps/api/js?sensor=false')
content = return_includes(response)
self.assertEqual(content, '')
#regr test for #628
# regr test for #628
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
@@ -147,7 +195,7 @@ class testResponse(unittest.TestCase):
def test_cookies(self):
current = setup_clean_session()
cookie = str(current.response.cookies)
session_key='%s=%s'%(current.response.session_id_name,current.response.session_id)
session_key = '%s=%s' % (current.response.session_id_name, current.response.session_id)
self.assertRegexpMatches(cookie, r'^Set-Cookie: ')
self.assertTrue(session_key in cookie)
self.assertTrue('Path=/' in cookie)
+50 -13
View File
@@ -11,11 +11,13 @@ import unittest
from gluon.html import A, ASSIGNJS, B, BEAUTIFY, P, BODY, BR, BUTTON, CAT, CENTER, CODE, COL, COLGROUP, DIV, SPAN, URL, verifyURL
from gluon.html import truncate_string, EM, FIELDSET, FORM, H1, H2, H3, H4, H5, H6, HEAD, HR, HTML, I, IFRAME, IMG, INPUT, EMBED
from gluon.html import LABEL, LEGEND, LI, LINK, MARKMIN, MENU, META, OBJECT, OL, OPTGROUP, OPTION, PRE, SCRIPT, SELECT, STRONG
from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, TH, TITLE, TT, UL, XHTML, XML
from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, TH, TITLE, TT, UL, XHTML, XML, web2pyHTMLParser
from gluon.storage import Storage
from gluon.html import XML_pickle, XML_unpickle
from gluon.html import TAG_pickler, TAG_unpickler
from gluon._compat import xrange, PY2, to_native
from gluon.decoder import decoder
import re
class TestBareHelpers(unittest.TestCase):
@@ -155,7 +157,7 @@ class TestBareHelpers(unittest.TestCase):
self.assertEqual(rtn, True)
# TODO: def test_XmlComponent(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for XML.__repr__")
def test_XML(self):
# sanitization process
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>').xml(),
@@ -168,6 +170,8 @@ class TestBareHelpers(unittest.TestCase):
# seams that __repr__ is no longer enough
##self.assertEqual(XML('1.3'), '1.3')
self.assertEqual(XML(u'<div>è</div>').xml(), b'<div>\xc3\xa8</div>')
# make sure unicode works with sanitize
self.assertEqual(XML(u'<div>è</div>', sanitize=True).xml(), b'<div>\xc3\xa8</div>')
# you can calc len on the class, that equals the xml() and the str()
##self.assertEqual(len(XML('1.3')), len('1.3'))
self.assertEqual(len(XML('1.3').xml()), len('1.3'))
@@ -179,19 +183,18 @@ class TestBareHelpers(unittest.TestCase):
# you can compare them
##self.assertEqual(XML('a') == XML('a'), True)
# beware that the comparison is made on the XML repr
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>', sanitize=True),
XML('<h1>HelloWorld</h1>'))
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>', sanitize=True).__repr__(),
XML('<h1>HelloWorld</h1>').__repr__())
# bug check for the sanitizer for closing no-close tags
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True),
XML('<p>Test</p><br /><p>Test</p><br />'))
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True).xml(),
XML('<p>Test</p><br /><p>Test</p><br />').xml())
# basic flatten test
self.assertEqual(XML('<p>Test</p>').flatten(), '<p>Test</p>')
self.assertEqual(XML('<p>Test</p>').flatten(render=lambda text, tag, attr: text), '<p>Test</p>')
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for XML_unpickle.__repr__")
def test_XML_pickle_unpickle(self):
# weird test
self.assertEqual(XML_unpickle(XML_pickle('data to be pickle')[1][0]), 'data to be pickle')
self.assertEqual(str(XML_unpickle(XML_pickle('data to be pickle')[1][0])), 'data to be pickle')
def test_DIV(self):
# Empty DIV()
@@ -255,6 +258,11 @@ class TestBareHelpers(unittest.TestCase):
self.assertEqual(DIV('<p>Test</p>', _class="class_test").get('_class'), 'class_test')
self.assertEqual(DIV(b'a').xml(), b'<div>a</div>')
def test_decoder(self):
tag_html = '<div><span><a id="1-1" u:v="$">hello</a></span><p class="this is a test">world</p></div>'
a = decoder(tag_html)
self.assertEqual(a, tag_html)
def test_CAT(self):
# Empty CAT()
self.assertEqual(CAT().xml(), b'')
@@ -636,8 +644,8 @@ class TestBareHelpers(unittest.TestCase):
# These 2 crash AppVeyor and Travis with: "ImportError: No YAML serializer available"
# self.assertEqual(FORM('<>', _a='1', _b='2').as_yaml(),
# "accepted: null\nattributes: {_a: '1', _action: '#', _b: '2', _enctype: multipart/form-data, _method: post}\ncomponents: [<>]\nerrors: {}\nlatest: {}\nparent: null\nvars: {}\n")
# self.assertEqual(FORM('<>', _a='1', _b='2').as_xml(),
# '<?xml version="1.0" encoding="UTF-8"?><document><errors></errors><vars></vars><parent>None</parent><attributes><_enctype>multipart/form-data</_enctype><_action>#</_action><_b>2</_b><_a>1</_a><_method>post</_method></attributes><components><item>&amp;lt;&amp;gt;</item></components><accepted>None</accepted><latest></latest></document>')
# TODO check tags content
self.assertEqual(len(FORM('<>', _a='1', _b='2').as_xml()), 334)
def test_BEAUTIFY(self):
#self.assertEqual(BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml(),
@@ -670,13 +678,42 @@ class TestBareHelpers(unittest.TestCase):
# TODO: def test_embed64(self):
# TODO: def test_web2pyHTMLParser(self):
def test_web2pyHTMLParser(self):
#tag should not be a byte
self.assertEqual(web2pyHTMLParser("<div></div>").tree.components[0].tag, 'div')
a = str(web2pyHTMLParser('<div>a<span>b</div>c').tree)
self.assertEqual(a, "<div>a<span>b</span></div>c")
tree = web2pyHTMLParser('hello<div a="b">world</div>').tree
tree.element(_a='b')['_c']=5
self.assertEqual(str(tree), 'hello<div a="b" c="5">world</div>')
a = str(web2pyHTMLParser('<div><img class="img"/></div>', closed=['img']).tree)
self.assertEqual(a, '<div><img class="img" /></div>')
#greater-than sign ( > ) --> decimal &#62; --> hexadecimal &#x3E;
#Less-than sign ( < ) --> decimal &#60; --> hexadecimal &#x3C;
# test decimal
a = str(web2pyHTMLParser('<div>&#60; &#62;</div>').tree)
self.assertEqual(a, '<div>&lt; &gt;</div>')
# test hexadecimal
a = str(web2pyHTMLParser('<div>&#x3C; &#x3E;</div>').tree)
self.assertEqual(a, '<div>&lt; &gt;</div>')
def test_markdown(self):
def markdown(text, tag=None, attributes={}):
r = {None: re.sub('\s+',' ',text), \
'h1':'#'+text+'\\n\\n', \
'p':text+'\\n'}.get(tag,text)
return r
a=TAG('<h1>Header</h1><p>this is a test</p>')
ret = a.flatten(markdown)
self.assertEqual(ret, '#Header\\n\\nthis is a test\\n')
# TODO: def test_markdown_serializer(self):
# TODO: def test_markmin_serializer(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for MARKMIN")
def test_MARKMIN(self):
# This test pass with python 2.7 but expected to fail under 2.6
# with self.assertRaises(TypeError) as cm:
+39 -39
View File
@@ -586,81 +586,81 @@ class TestUnicode(unittest.TestCase):
# disables prepending the scheme in the return value
def testUnicodeToAsciiUrl(self):
self.assertEquals(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
self.assertEquals(
self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
self.assertEqual(
unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca')
self.assertRaises(UnicodeError, unicode_to_ascii_authority,
u'\u4e2d' * 1000) # label is too long
def testValidUrls(self):
self.assertEquals(self.x(u'www.Alliancefrancaise.nu'), (
self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), (
'http://www.Alliancefrancaise.nu', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu'), (
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu'), (
'http://www.xn--alliancefranaise-npb.nu', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu:8080'), (
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu:8080'), (
'http://www.xn--alliancefranaise-npb.nu:8080', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu'),
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu'),
('http://www.xn--alliancefranaise-npb.nu', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(
u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'),
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'),
('http://xn--fiq13b.com/%4e%86', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None))
self.assertEquals(
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None))
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None))
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None))
self.assertEqual(
self.x(u'http://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None))
self.assertEquals(self.x(u'http://\xe4\xf6\xfc\xdf.com'), (
self.assertEqual(self.x(u'http://\xe4\xf6\xfc\xdf.com'), (
'http://xn--ss-uia6e4a.com', None))
self.assertEquals(self.x(
self.assertEqual(self.x(
u'http://visegr\xe1d.com'), ('http://xn--visegrd-mwa.com', None))
self.assertEquals(self.x(u'http://h\xe1zipatika.com'), (
self.assertEqual(self.x(u'http://h\xe1zipatika.com'), (
'http://xn--hzipatika-01a.com', None))
self.assertEquals(self.x(u'http://www.\xe7ukurova.com'), (
self.assertEqual(self.x(u'http://www.\xe7ukurova.com'), (
'http://www.xn--ukurova-txa.com', None))
self.assertEquals(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None))
self.assertEquals(self.x(u'google.ca.'), ('http://google.ca.', None))
self.assertEqual(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None))
self.assertEqual(self.x(u'google.ca.'), ('http://google.ca.', None))
self.assertEquals(
self.assertEqual(
self.y(u'https://google.ca'), ('https://google.ca', None))
self.assertEquals(self.y(
self.assertEqual(self.y(
u'https://\u4e2d\u4fd4.com'), ('https://xn--fiq13b.com', None))
self.assertEquals(self.z(u'google.ca'), ('google.ca', None))
self.assertEqual(self.z(u'google.ca'), ('google.ca', None))
def testInvalidUrls(self):
self.assertEquals(
self.assertEqual(
self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.dne'), (
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.dne'), (
u'http://\u4e2d\u4fd4.dne', 'Enter a valid URL'))
self.assertEquals(self.x(u'https://google.dne'), (
self.assertEqual(self.x(u'https://google.dne'), (
u'https://google.dne', 'Enter a valid URL'))
self.assertEquals(self.x(u'https://google..ca'), (
self.assertEqual(self.x(u'https://google..ca'), (
u'https://google..ca', 'Enter a valid URL'))
self.assertEquals(
self.assertEqual(
self.x(u'google..ca'), (u'google..ca', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), (
self.assertEqual(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), (
u'http://' + u'\u4e2d' * 1000 + u'.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://google.com#fragment_\u4e86'), (
self.assertEqual(self.x(u'http://google.com#fragment_\u4e86'), (
u'http://google.com#fragment_\u4e86', 'Enter a valid URL'))
self.assertEquals(self.x(u'http\u4e86://google.com'), (
self.assertEqual(self.x(u'http\u4e86://google.com'), (
u'http\u4e86://google.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http\u4e86://google.com#fragment_\u4e86'), (
self.assertEqual(self.x(u'http\u4e86://google.com#fragment_\u4e86'), (
u'http\u4e86://google.com#fragment_\u4e86', 'Enter a valid URL'))
self.assertEquals(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), (
self.assertEqual(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), (
u'http://\u4e2d\u4fd4.com/\u4e86', 'Enter a valid URL'))
#self.assertEquals(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL'))
#self.assertEqual(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL'))
self.assertEquals(self.z(u'invalid.domain..com'), (
self.assertEqual(self.z(u'invalid.domain..com'), (
u'invalid.domain..com', 'Enter a valid URL'))
self.assertEquals(self.z(u'invalid.\u4e2d\u4fd4.blargg'), (
self.assertEqual(self.z(u'invalid.\u4e2d\u4fd4.blargg'), (
u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL'))
# ##############################################################################
+2 -2
View File
@@ -95,14 +95,14 @@ class TestRouter(unittest.TestCase):
""" Test router syntax error """
level = logger.getEffectiveLevel()
logger.setLevel(logging.CRITICAL) # disable logging temporarily
self.assertRaises(SyntaxError, load, data='x:y')
self.assertRaises(SyntaxError, load, data='x::y')
self.assertRaises(
SyntaxError, load, rdict=dict(BASE=dict(badkey="value")))
self.assertRaises(SyntaxError, load, rdict=dict(
BASE=dict(), app=dict(default_application="name")))
self.myassertRaisesRegex(SyntaxError, "invalid syntax",
load, data='x:y')
load, data='x::y')
self.myassertRaisesRegex(SyntaxError, "unknown key",
load, rdict=dict(BASE=dict(badkey="value")))
self.myassertRaisesRegex(SyntaxError, "BASE-only key",
+22 -8
View File
@@ -235,6 +235,11 @@ class TestValidators(unittest.TestCase):
self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')])
rtn = IS_IN_DB(db, db.person.id, db.person.name, error_message='oops', sort=True).options(zero=True)
self.assertEqual(rtn, [('', ''), ('%d' % costanza_id, 'costanza'), ('%d' % george_id, 'george')])
# Test None
rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(None)
self.assertEqual(rtn, (None, 'oops'))
rtn = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')(None)
self.assertEqual(rtn, (None, 'oops'))
# Test using the set it made for options
vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')
vldtr.options()
@@ -434,21 +439,21 @@ class TestValidators(unittest.TestCase):
rtn = IS_NOT_EMPTY()('x')
self.assertEqual(rtn, ('x', None))
rtn = IS_NOT_EMPTY()(' x ')
self.assertEqual(rtn, ('x', None))
self.assertEqual(rtn, (' x ', None))
rtn = IS_NOT_EMPTY()(None)
self.assertEqual(rtn, (None, 'Enter a value'))
rtn = IS_NOT_EMPTY()('')
self.assertEqual(rtn, ('', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' ')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, (' ', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' \n\t')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, (' \n\t', 'Enter a value'))
rtn = IS_NOT_EMPTY()([])
self.assertEqual(rtn, ([], 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='def')('def')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, ('def', 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, ('deg', 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='def')('abc')
self.assertEqual(rtn, ('abc', None))
@@ -531,6 +536,11 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMAIL(error_message='oops')(42)
self.assertEqual(rtn, (42, 'oops'))
# test for Internationalized Domain Names, see https://docs.python.org/2/library/codecs.html#module-encodings.idna
rtn = IS_EMAIL()('web2py@Alliancefrançaise.nu')
self.assertEqual(rtn, ('web2py@Alliancefrançaise.nu', None))
def test_IS_LIST_OF_EMAILS(self):
emails = ['localguy@localhost', '_Yosemite.Sam@example.com']
rtn = IS_LIST_OF_EMAILS()(','.join(emails))
@@ -702,15 +712,19 @@ class TestValidators(unittest.TestCase):
def test_IS_LOWER(self):
rtn = IS_LOWER()('ABC')
self.assertEqual(rtn, ('abc', None))
rtn = IS_LOWER()(b'ABC')
self.assertEqual(rtn, (b'abc', None))
rtn = IS_LOWER()('Ñ')
self.assertEqual(rtn, (b'\xc3\xb1', None))
self.assertEqual(rtn, ('ñ', None))
def test_IS_UPPER(self):
rtn = IS_UPPER()('abc')
self.assertEqual(rtn, ('ABC', None))
rtn = IS_UPPER()(b'abc')
self.assertEqual(rtn, (b'ABC', None))
rtn = IS_UPPER()('ñ')
self.assertEqual(rtn, (b'\xc3\x91', None))
self.assertEqual(rtn, ('Ñ', None))
def test_IS_SLUG(self):
rtn = IS_SLUG()('abc123')
@@ -780,7 +794,7 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMPTY_OR(IS_EMAIL())('abc')
self.assertEqual(rtn, ('abc', 'Enter a valid email address'))
rtn = IS_EMPTY_OR(IS_EMAIL())(' abc ')
self.assertEqual(rtn, ('abc', 'Enter a valid email address'))
self.assertEqual(rtn, (' abc ', 'Enter a valid email address'))
rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options(zero=False)
self.assertEqual(rtn, [('', ''), ('id1', 'first label'), ('id2', 'second label')])
rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options()
+1 -1
View File
@@ -110,7 +110,7 @@ class TestWeb(LiveTest):
self.assertTrue('Welcome Homer' in client.text)
client = WebClient('http://127.0.0.1:8000/admin/default/')
client.post('index', data=dict(password='hello'))
client.post('index', data=dict(password='testpass'))
client.get('site')
client.get('design/welcome')
+287 -851
View File
File diff suppressed because it is too large Load Diff
+118 -98
View File
@@ -10,7 +10,6 @@
Validators
-----------
"""
import os
import re
import datetime
@@ -21,7 +20,8 @@ import urllib
import struct
import decimal
import unicodedata
from gluon._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native
from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native, string_types, urlparse
from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
from pydal.objects import Field, FieldVirtual, FieldMethod
from functools import reduce
@@ -195,7 +195,7 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode or (not(PY2))
def __call__(self, value):
if not(PY2): # PY3 convert bytes to unicode
if not(PY2): # PY3 convert bytes to unicode
value = to_unicode(value)
if self.is_unicode or not(PY2):
@@ -270,7 +270,7 @@ class IS_EXPR(Validator):
return (value, self.expression(value))
# for backward compatibility
self.environment.update(value=value)
exec ('__ret__=' + self.expression, self.environment)
exec('__ret__=' + self.expression, self.environment)
if self.environment['__ret__']:
return (value, None)
return (value, translate(self.error_message))
@@ -659,7 +659,7 @@ class IS_IN_DB(Validator):
return (values, None)
else:
if field.type in ('id', 'integer'):
if isinstance(value, (int, long)) or value.isdigit():
if isinstance(value, (int, long)) or (isinstance(value, string_types) and value.isdigit()):
value = int(value)
elif self.auto_add:
value = self.maybe_add(table, self.fieldnames[0], value)
@@ -989,14 +989,15 @@ class IS_DECIMAL_IN_RANGE(Validator):
def is_empty(value, empty_regex=None):
_value = value
"""test empty field"""
if isinstance(value, (str, unicodeT)):
value = value.strip()
if empty_regex is not None and empty_regex.match(value):
value = ''
if value is None or value == '' or value == []:
return (value, True)
return (value, False)
return (_value, True)
return (_value, False)
class IS_NOT_EMPTY(Validator):
@@ -1156,15 +1157,16 @@ class IS_EMAIL(Validator):
"""
regex = re.compile('''
body_regex = re.compile('''
^(?!\.) # name may not begin with a dot
(
[-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot
|
(?<!\.)\. # single dots only
)+
(?<!\.) # name may not end with a dot
@
(?<!\.)$ # name may not end with a dot
''', re.VERBOSE | re.IGNORECASE)
domain_regex = re.compile('''
(
localhost
|
@@ -1196,14 +1198,27 @@ class IS_EMAIL(Validator):
self.error_message = error_message
def __call__(self, value):
if not(isinstance(value, (basestring, unicodeT))) or not value or '@' not in value:
return (value, translate(self.error_message))
body, domain = value.rsplit('@', 1)
try:
match = self.regex.match(value)
except TypeError:
match_body = self.body_regex.match(body)
match_domain = self.domain_regex.match(domain)
if not match_domain:
# check for Internationalized Domain Names
# see https://docs.python.org/2/library/codecs.html#module-encodings.idna
domain_encoded = to_unicode(domain).encode('idna').decode('ascii')
match_domain = self.domain_regex.match(domain_encoded)
match = (match_body != None) and (match_domain != None)
except (TypeError, UnicodeError):
# Value may not be a string where we can look for matches.
# Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date.
match = None
if match:
domain = value.split('@')[1]
if (not self.banned or not self.banned.match(domain)) \
and (not self.forced or self.forced.match(domain)):
return (value, None)
@@ -1372,19 +1387,6 @@ unofficial_url_schemes = [
all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
http_schemes = [None, 'http', 'https']
# This regex comes from RFC 2396, Appendix B. It's used to split a URL into
# its component parts
# Here are the regex groups that it extracts:
# scheme = group(2)
# authority = group(4)
# path = group(5)
# query = group(7)
# fragment = group(9)
url_split_regex = \
re.compile('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?')
# Defined in RFC 3490, Section 3.1, Requirement #1
# Use this regex to split the authority component of a unicode URL into
# its component labels
@@ -1454,18 +1456,15 @@ def unicode_to_ascii_authority(authority):
# We use the ToASCII operation because we are about to put the authority
# into an IDN-unaware slot
asciiLabels = []
try:
import encodings.idna
for label in labels:
if label:
asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
else:
# encodings.idna.ToASCII does not accept an empty string, but
# it is necessary for us to allow for empty labels so that we
# don't modify the URL
asciiLabels.append('')
except:
asciiLabels = [str(label) for label in labels]
import encodings.idna
for label in labels:
if label:
asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
else:
# encodings.idna.ToASCII does not accept an empty string, but
# it is necessary for us to allow for empty labels so that we
# don't modify the URL
asciiLabels.append('')
# RFC 3490, Section 4, Step 5
return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
@@ -1504,33 +1503,35 @@ def unicode_to_ascii_url(url, prepend_scheme):
"""
# convert the authority component of the URL into an ASCII punycode string,
# but encode the rest using the regular URI character encoding
groups = url_split_regex.match(url).groups()
components = urlparse.urlparse(url)
prepended = False
# If no authority was found
if not groups[3]:
if not components.netloc:
# Try appending a scheme to see if that fixes the problem
scheme_to_prepend = prepend_scheme or 'http'
groups = url_split_regex.match(
to_unicode(scheme_to_prepend) + u'://' + url).groups()
components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url)
prepended = True
# if we still can't find the authority
if not groups[3]:
if not components.netloc:
raise Exception('No authority component found, ' +
'could not decode unicode to US-ASCII')
# We're here if we found an authority, let's rebuild the URL
scheme = groups[1]
authority = groups[3]
path = groups[4] or ''
query = groups[5] or ''
fragment = groups[7] or ''
scheme = components.scheme
authority = components.netloc
path = components.path
query = components.query
fragment = components.fragment
if prepend_scheme:
scheme = str(scheme) + '://'
else:
if prepended:
scheme = ''
return scheme + unicode_to_ascii_authority(authority) +\
escape_unicode(path) + escape_unicode(query) + str(fragment)
unparsed = urlparse.urlunparse((scheme, unicode_to_ascii_authority(authority), escape_unicode(path), '', escape_unicode(query), str(fragment)))
if unparsed.startswith('//'):
unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning
return unparsed
class IS_GENERIC_URL(Validator):
@@ -1591,7 +1592,8 @@ class IS_GENERIC_URL(Validator):
% (self.prepend_scheme, self.allowed_schemes))
GENERIC_URL = re.compile(r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$")
GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%#]+$")
GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
URL_FRAGMENT_VALID = re.compile(r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
def __call__(self, value):
"""
@@ -1603,41 +1605,49 @@ class IS_GENERIC_URL(Validator):
prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message
"""
try:
# if the URL does not misuse the '%' character
if not self.GENERIC_URL.search(value):
# if the URL is only composed of valid characters
if self.GENERIC_URL_VALID.match(value):
# Then split up the URL into its components and check on
# the scheme
scheme = url_split_regex.match(value).group(2)
# Clean up the scheme before we check it
if not scheme is None:
scheme = urllib_unquote(scheme).lower()
# If the scheme really exists
if scheme in self.allowed_schemes:
# Then the URL is valid
return (value, None)
else:
# else, for the possible case of abbreviated URLs with
# ports, check to see if adding a valid scheme fixes
# the problem (but only do this if it doesn't have
# one already!)
if value.find('://') < 0 and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(
schemeToUse + '://' + value)
# if the prepend test succeeded
if prependTest[1] is None:
# if prepending in the output is enabled
if self.prepend_scheme:
return prependTest
else:
# else return the original,
# non-prepended value
return (value, None)
except:
pass
# if we dont have anything or the URL misuses the '%' character
if not value or self.GENERIC_URL.search(value):
return (value, translate(self.error_message))
if '#' in value:
url, fragment_part = value.split('#')
else:
url, fragment_part = value, ''
# if the URL is only composed of valid characters
if self.GENERIC_URL_VALID.match(url) and (not fragment_part or self.URL_FRAGMENT_VALID.match(fragment_part)):
# Then parse the URL into its components and check on
try:
components = urlparse.urlparse(urllib_unquote(value))._asdict()
except ValueError:
return (value, translate(self.error_message))
# Clean up the scheme before we check it
scheme = components['scheme']
if len(scheme) == 0:
scheme = None
else:
scheme = components['scheme'].lower()
# If the scheme doesn't really exists
if scheme not in self.allowed_schemes or not scheme and ':' in components['path']:
# for the possible case of abbreviated URLs with
# ports, check to see if adding a valid scheme fixes
# the problem (but only do this if it doesn't have
# one already!)
if '://' not in value and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(
schemeToUse + '://' + value)
# if the prepend test succeeded
if prependTest[1] is None:
# if prepending in the output is enabled
if self.prepend_scheme:
return prependTest
else:
return (value, None)
else:
return (value, None)
# else the URL is not valid
return (value, translate(self.error_message))
@@ -1904,15 +1914,14 @@ class IS_HTTP_URL(Validator):
(possible prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message
"""
try:
# if the URL passes generic validation
x = IS_GENERIC_URL(error_message=self.error_message,
allowed_schemes=self.allowed_schemes,
prepend_scheme=self.prepend_scheme)
if x(value)[1] is None:
componentsMatch = url_split_regex.match(value)
authority = componentsMatch.group(4)
components = urlparse.urlparse(value)
authority = components.netloc
# if there is an authority component
if authority:
# if authority is a valid IP address
@@ -1932,7 +1941,7 @@ class IS_HTTP_URL(Validator):
else:
# else this is a relative/abbreviated URL, which will parse
# into the URL's path component
path = componentsMatch.group(5)
path = components.path
# relative case: if this is a valid path (if it starts with
# a slash)
if path.startswith('/'):
@@ -1941,7 +1950,7 @@ class IS_HTTP_URL(Validator):
else:
# abbreviated case: if we haven't already, prepend a
# scheme and see if it fixes the problem
if value.find('://') < 0:
if '://' not in value and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(schemeToUse
+ '://' + value)
@@ -2108,7 +2117,6 @@ class IS_URL(Validator):
# If we are not able to convert the unicode url into a
# US-ASCII URL, then the URL is not valid
return (value, translate(self.error_message))
methodResult = subMethod(asciiValue)
# if the validation of the US-ASCII version of the value failed
if not methodResult[1] is None:
@@ -2480,7 +2488,13 @@ class IS_LOWER(Validator):
"""
def __call__(self, value):
return (to_bytes(to_unicode(value).lower()), None)
cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).lower()
return (cast_back(value), None)
class IS_UPPER(Validator):
@@ -2495,7 +2509,13 @@ class IS_UPPER(Validator):
"""
def __call__(self, value):
return (to_bytes(to_unicode(value).upper()), None)
cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).upper()
return (cast_back(value), None)
def urlify(s, maxlen=80, keep_underscores=False):
+51 -40
View File
@@ -13,7 +13,7 @@ from __future__ import print_function
import datetime
import sys
from gluon._compat import StringIO, thread, xrange
from gluon._compat import StringIO, thread, xrange, PY2
import time
import threading
import os
@@ -134,24 +134,29 @@ class web2pyDialog(object):
def __init__(self, root, options):
""" web2pyDialog constructor """
import Tkinter
import tkMessageBox
if PY2:
import Tkinter as tkinter
import tkMessageBox as messagebox
else:
import tkinter
from tkinter import messagebox
bg_color = 'white'
root.withdraw()
self.root = Tkinter.Toplevel(root, bg=bg_color)
self.root = tkinter.Toplevel(root, bg=bg_color)
self.root.resizable(0, 0)
self.root.title(ProgramName)
self.options = options
self.scheduler_processes = {}
self.menu = Tkinter.Menu(self.root)
servermenu = Tkinter.Menu(self.menu, tearoff=0)
self.menu = tkinter.Menu(self.root)
servermenu = tkinter.Menu(self.menu, tearoff=0)
httplog = os.path.join(self.options.folder, self.options.log_filename)
iconphoto = os.path.join('extras', 'icons', 'web2py.gif')
if os.path.exists(iconphoto):
img = Tkinter.PhotoImage(file=iconphoto)
img = tkinter.PhotoImage(file=iconphoto)
self.root.tk.call('wm', 'iconphoto', self.root._w, img)
# Building the Menu
item = lambda: start_browser(httplog)
@@ -163,16 +168,16 @@ class web2pyDialog(object):
self.menu.add_cascade(label='Server', menu=servermenu)
self.pagesmenu = Tkinter.Menu(self.menu, tearoff=0)
self.pagesmenu = tkinter.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label='Pages', menu=self.pagesmenu)
#scheduler menu
self.schedmenu = Tkinter.Menu(self.menu, tearoff=0)
self.schedmenu = tkinter.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label='Scheduler', menu=self.schedmenu)
#start and register schedulers from options
self.update_schedulers(start=True)
helpmenu = Tkinter.Menu(self.menu, tearoff=0)
helpmenu = tkinter.Menu(self.menu, tearoff=0)
# Home Page
item = lambda: start_browser('http://www.web2py.com/')
@@ -180,7 +185,7 @@ class web2pyDialog(object):
command=item)
# About
item = lambda: tkMessageBox.showinfo('About web2py', ProgramInfo)
item = lambda: messagebox.showinfo('About web2py', ProgramInfo)
helpmenu.add_command(label='About',
command=item)
@@ -194,10 +199,10 @@ class web2pyDialog(object):
else:
self.root.protocol('WM_DELETE_WINDOW', self.quit)
sticky = Tkinter.NW
sticky = tkinter.NW
# Prepare the logo area
self.logoarea = Tkinter.Canvas(self.root,
self.logoarea = tkinter.Canvas(self.root,
background=bg_color,
width=300,
height=300)
@@ -206,22 +211,22 @@ class web2pyDialog(object):
logo = os.path.join('extras', 'icons', 'splashlogo.gif')
if os.path.exists(logo):
img = Tkinter.PhotoImage(file=logo)
pnl = Tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0)
img = tkinter.PhotoImage(file=logo)
pnl = tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0)
pnl.pack(side='top', fill='both', expand='yes')
# Prevent garbage collection of img
pnl.image = img
# Prepare the banner area
self.bannerarea = Tkinter.Canvas(self.root,
self.bannerarea = tkinter.Canvas(self.root,
bg=bg_color,
width=300,
height=300)
self.bannerarea.grid(row=1, column=1, columnspan=2, sticky=sticky)
Tkinter.Label(self.bannerarea, anchor=Tkinter.N,
tkinter.Label(self.bannerarea, anchor=tkinter.N,
text=str(ProgramVersion + "\n" + ProgramAuthor),
font=('Helvetica', 11), justify=Tkinter.CENTER,
font=('Helvetica', 11), justify=tkinter.CENTER,
foreground='#195866', background=bg_color,
height=3).pack(side='top',
fill='both',
@@ -230,24 +235,24 @@ class web2pyDialog(object):
self.bannerarea.after(1000, self.update_canvas)
# IP
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Server IP:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=4,
justify=tkinter.RIGHT).grid(row=4,
column=1,
sticky=sticky)
self.ips = {}
self.selected_ip = Tkinter.StringVar()
self.selected_ip = tkinter.StringVar()
row = 4
ips = [('127.0.0.1', 'Local (IPv4)')] + \
([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \
[(ip, 'Public') for ip in options.ips] + \
[('0.0.0.0', 'Public')]
for ip, legend in ips:
self.ips[ip] = Tkinter.Radiobutton(
self.ips[ip] = tkinter.Radiobutton(
self.root, bg=bg_color, highlightthickness=0,
selectcolor='light grey', width=30,
anchor=Tkinter.W, text='%s (%s)' % (legend, ip),
justify=Tkinter.LEFT,
anchor=tkinter.W, text='%s (%s)' % (legend, ip),
justify=tkinter.LEFT,
variable=self.selected_ip, value=ip)
self.ips[ip].grid(row=row, column=2, sticky=sticky)
if row == 4:
@@ -256,30 +261,30 @@ class web2pyDialog(object):
shift = row
# Port
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Server Port:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift,
justify=tkinter.RIGHT).grid(row=shift,
column=1, pady=10,
sticky=sticky)
self.port_number = Tkinter.Entry(self.root)
self.port_number.insert(Tkinter.END, self.options.port)
self.port_number = tkinter.Entry(self.root)
self.port_number.insert(tkinter.END, self.options.port)
self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10)
# Password
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Choose Password:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift + 1,
justify=tkinter.RIGHT).grid(row=shift + 1,
column=1,
sticky=sticky)
self.password = Tkinter.Entry(self.root, show='*')
self.password = tkinter.Entry(self.root, show='*')
self.password.bind('<Return>', lambda e: self.start())
self.password.focus_force()
self.password.grid(row=shift + 1, column=2, sticky=sticky)
# Prepare the canvas
self.canvas = Tkinter.Canvas(self.root,
self.canvas = tkinter.Canvas(self.root,
width=400,
height=100,
bg='black')
@@ -288,19 +293,19 @@ class web2pyDialog(object):
self.canvas.after(1000, self.update_canvas)
# Prepare the frame
frame = Tkinter.Frame(self.root)
frame = tkinter.Frame(self.root)
frame.grid(row=shift + 3, column=1, columnspan=2, pady=5,
sticky=sticky)
# Start button
self.button_start = Tkinter.Button(frame,
self.button_start = tkinter.Button(frame,
text='start server',
command=self.start)
self.button_start.grid(row=0, column=0, sticky=sticky)
# Stop button
self.button_stop = Tkinter.Button(frame,
self.button_stop = tkinter.Button(frame,
text='stop server',
command=self.stop)
@@ -454,9 +459,12 @@ class web2pyDialog(object):
def error(self, message):
""" Shows error message """
import tkMessageBox
tkMessageBox.showerror('web2py start server', message)
if PY2:
import tkMessageBox as messagebox
else:
from tkinter import messagebox
messagebox.showerror('web2py start server', message)
def start(self):
""" Starts web2py server """
@@ -1191,10 +1199,13 @@ def start(cron=True):
if not options.nogui and options.password == '<ask>':
try:
import Tkinter
if PY2:
import Tkinter as tkinter
else:
import tkinter
havetk = True
try:
root = Tkinter.Tk()
root = tkinter.Tk()
except:
pass
except (ImportError, OSError):
+2 -1
View File
@@ -15,6 +15,7 @@ except ImportError:
import readline
try:
from gluon import DAL
from gluon.fileutils import open_file
except ImportError as err:
print('gluon path not found')
@@ -531,7 +532,7 @@ class setCopyDB():
self.db.export_to_csv_file(open('tmp.sql', 'wb'))
print 'importing data...'
other_db.import_from_csv_file(open('tmp.sql', 'rb'))
other_db.import_from_csv_file(open_file('tmp.sql', 'rb'))
other_db.commit()
print 'done!'
print 'Attention: do not run this program again or you end up with duplicate records'