Compare commits

...
233 Commits
Author SHA1 Message Date
mdipierro 0a013e3edc R-2.15.2 2017-07-19 01:21:48 -05:00
mdipierroandGitHub fb4c114d85 Merge pull request #1686 from leonelcamara/25issues
Fixes 6 issues
2017-07-18 03:44:19 -05:00
mdipierroandGitHub b47e1334d5 Merge pull request #1682 from leonelcamara/fixheroku
Fixheroku
2017-07-18 03:43:45 -05:00
Leonel Câmara ce0b255747 Fixes #1672 2017-07-16 17:59:52 +01:00
Leonel Câmara 05d2ced779 remove print that was left from debugging 2017-07-16 14:27:11 +01:00
Leonel Câmara d144ff7d65 Fixes #1687 2017-07-16 14:06:03 +01:00
Leonel Câmara 780510dc32 Fixes #1684 2017-07-15 09:52:58 +01:00
Leonel Câmara 7a5f611e76 Fixes #1571 2017-07-14 20:23:13 +01:00
Leonel Câmara b7b8a009f2 Fixes #1680 2017-07-14 20:17:30 +01:00
Leonel Câmara 113df51ef9 Fixes #1685 2017-07-14 18:52:36 +01:00
mdipierro 4e704ca6f7 R-2.15.1 2017-07-10 16:18:35 -05:00
mdipierro 047ed786ac fixed some error in hashlib_md5 2017-07-10 16:02:15 -05:00
mdipierro ca1e5156ba fixed hashlib_md5 again, thanks Paolo 2017-07-10 15:10:16 -05:00
mdipierro 4854b84ff9 fixed a python 3 problem, thanks kato 2017-07-10 14:54:08 -05:00
mdipierro aa252cdbd8 Merge branch 'BuhtigithuB-improve/pep8-globals-py' 2017-07-10 14:20:45 -05:00
mdipierro e3ec4d4075 merged hashlib conflict 2017-07-10 14:20:39 -05:00
mdipierro 81b000d47a Merge branch 'master' of github.com:web2py/web2py 2017-07-10 14:19:08 -05:00
mdipierro 305dac4976 fixed changelog 2017-07-10 14:19:01 -05:00
mdipierroandGitHub 876cc634a8 Merge pull request #1679 from leonelcamara/fix1675
Fixes #1675 dropdown not working in the "manage" button
2017-07-10 14:18:17 -05:00
mdipierroandGitHub c004d2c16e Merge pull request #1678 from willimoa/issue/1674
Issue/1674
2017-07-10 14:17:56 -05:00
mdipierroandGitHub e3cce4d752 Merge pull request #1669 from ilvalle/testing_pypy3_trusty
Testing pypy3.5 in travis, switching to ubuntu trusty
2017-07-10 14:17:20 -05:00
mdipierroandGitHub 2c84b88466 Merge pull request #1668 from ilvalle/fix_compile_views
fix performance regression with compiled views
2017-07-10 14:17:04 -05:00
mdipierroandGitHub fe652c851b Merge pull request #1667 from BuhtigithuB/improve/pep8-main-py
Enhance main.py PEP8
2017-07-10 14:16:09 -05:00
mdipierroandGitHub c183c7b18b Merge pull request #1666 from abastardi/ajax-uploads
Enable Ajax file uploads
2017-07-10 14:15:01 -05:00
mdipierroandGitHub e2c9875cd5 Merge pull request #1665 from BuhtigithuB/clean/python-2-6-remaining-code-in-import-all-py
Close #1664 remaining py2.6 specific code
2017-07-10 14:14:44 -05:00
mdipierroandGitHub 579c54f926 Merge pull request #1663 from BuhtigithuB/improve/pep8-http-py
Enhance http.py PEP8
2017-07-10 14:14:09 -05:00
mdipierroandGitHub b2cb0bc189 Merge pull request #1662 from BuhtigithuB/improve/pep8-html-py
Enhance html.py PEP8
2017-07-10 14:13:53 -05:00
mdipierroandGitHub 3f6e8c755a Merge pull request #1660 from BuhtigithuB/improve/pep8-highlight-py
Enhance highlight.py PEP8
2017-07-10 14:13:19 -05:00
mdipierroandGitHub 023d7f3e6c Merge pull request #1659 from BuhtigithuB/improve/pep8-decoder-py
Enhance decoder.py PEP8
2017-07-10 14:13:04 -05:00
mdipierroandGitHub 01285ad4cd Merge pull request #1657 from BuhtigithuB/improve/pep8-dal-py
Enhance dal.py PEP8
2017-07-10 14:12:55 -05:00
mdipierroandGitHub 0d855c1e9c Merge pull request #1656 from BuhtigithuB/improve/pep8-contenttype-py
Enhance contenttype.py PEP8
2017-07-10 14:12:45 -05:00
mdipierroandGitHub 2d21c00e8d Merge pull request #1655 from BuhtigithuB/improve/pep8-compileapp-py
Enhance compileapp.py PEP8
2017-07-10 14:12:30 -05:00
mdipierroandGitHub de9d0eb895 Merge pull request #1654 from BuhtigithuB/improve/pep8-admin-py
Enhance admin.py PEP8
2017-07-10 14:12:16 -05:00
mdipierroandGitHub 9998916ef6 Merge pull request #1653 from BuhtigithuB/improve/pep8-validators-py
Enhance validators.py PEP8 and fix docstring py3 compatibility
2017-07-10 14:11:58 -05:00
mdipierroandGitHub 453123a8ed Merge pull request #1652 from BuhtigithuB/improve/pep8-tools-py
Enhance tools.py PEP8 compliancy
2017-07-10 14:11:11 -05:00
mdipierroandGitHub 2396cad2d1 Merge pull request #1658 from ilvalle/fix_minify
fix minify, added tests
2017-07-10 14:09:53 -05:00
mdipierro 540eecc207 R-2.15.0b2 2017-07-10 03:23:13 -05:00
mdipierro 83681f3f5d fixed markmin test, change beahvior 2017-07-10 03:16:42 -05:00
mdipierro b0a01ef720 fixed some relative imports, thanks steve.van.christen 2017-07-10 03:13:12 -05:00
mdipierro da5543f62e tracking newest pydal 2017-07-10 03:06:08 -05:00
mdipierro dc4ff7c3cc made markmin2html independens on gluon again and fixed reported problem, thanks mweissen 2017-07-10 03:01:35 -05:00
mdipierro 0223b0871e jquery 2.2.4 in admin 2017-07-10 02:43:56 -05:00
Leonel Câmara b3a7c20f3f Fixes #1635 2017-07-10 00:17:15 +01:00
Leonel Câmara 2fc4115718 update heroku.py to the new pydal 2017-07-09 15:35:48 +01:00
Leonel Câmara c6c027dbec Fixes #1675 dropdown not working in the "manage" button 2017-07-07 18:10:31 +01:00
Andrew Willimott 60edb11420 Explicit path added for graph css file. 2017-07-07 21:03:55 +12:00
Andrew Willimott 51ce3ffd36 Path to css file changed to explicitly point to admin app 2017-07-07 20:39:42 +12:00
mdipierro 89832479fc R-2.16.0b1 2017-07-05 01:48:28 -05:00
mdipierro 15ffdd9a20 R-2.16.0b1 2017-07-05 01:46:01 -05:00
mdipierro 8505c7b282 R-2.16.0b1 2017-07-05 01:42:59 -05:00
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
ilvalle af7bfac1e2 Testing pypy3.5 in travis, switching to ubuntu trusty 2017-06-24 08:07:50 +02:00
ilvalle 054320820c fix performance regression with compiled views 2017-06-24 07:21:23 +02:00
Richard Vézina 1a52b0ee3b Enhance main.py PEP8 2017-06-22 09:46:55 -04:00
abastardi 78f3af6fc1 Use FormData when supported to allow Ajax file uploads 2017-06-21 16:29:19 -04:00
Richard Vézina b5b98d6e19 Close #1664 remaining py2.6 specific code 2017-06-21 15:32:28 -04:00
Richard Vézina aa1b71e431 Enhance http.py PEP8 2017-06-21 15:14:41 -04:00
Richard Vézina 472c0ff2fb Enhance html.py PEP8 2017-06-21 15:08:52 -04:00
Richard Vézina 58bedd4c1a Enhance highlight.py PEP8 2017-06-21 14:54:59 -04:00
Richard Vézina 88790dcaee Enhance globals.py PEP8 2017-06-21 14:49:10 -04:00
Richard Vézina a8fb41333b Enhance decoder.py PEP8 2017-06-21 14:38:52 -04:00
Richard Vézina 8d5464692f Enhance dal.py PEP8 2017-06-21 13:41:10 -04:00
Richard Vézina 2080e0460f Enhance contenttype.py PEP8 2017-06-21 13:32:42 -04:00
Richard Vézina 7f5fc798c5 Enhance compileapp.py PEP8 2017-06-21 13:30:02 -04:00
Richard Vézina 833cb03ee1 Enhance admin.py PEP8 2017-06-21 13:22:30 -04:00
Richard Vézina 590de9c890 Enhance validators.py PEP8 and fix docstring py3 compatibility 2017-06-21 11:57:31 -04:00
Richard Vézina 583d106104 Fix docstring py3 compatibility issues print -> print() 2017-06-21 11:33:00 -04:00
Richard Vézina 7ada2cf89a Enhance tools.py PEP8 compliancy 2017-06-21 11:27:54 -04:00
mdipierroandGitHub 2f0b429f9e Merge pull request #1651 from BuhtigithuB/improve/pep8-authapi-py
Enhance authapi.py PEP8 compliancy
2017-06-20 16:44:18 -05:00
ilvalle 9f79dccb05 fix minify, added tests 2017-06-20 22:12:43 +02: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
mdipierroandGitHub 566feb79d4 Merge pull request #1649 from ilvalle/fix_globals
fix Pickler.dispatch_table
2017-06-15 04:01:14 -05:00
mdipierroandGitHub 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âmaraandGitHub 7c9653ae23 Merge branch 'master' into test_is_url_py3 2017-06-14 15:41:07 +01:00
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 30c28ad44c Merge pull request #1630 from ilvalle/fix_1629
fix web debugger
2017-06-05 11:41:25 -05:00
mdipierroandGitHub d905968197 Merge pull request #1626 from willimoa/issue/1472
Issue/1472
2017-06-05 11:40:38 -05:00
mdipierroandGitHub f1ef95e15f Merge pull request #1625 from Scimonster/master
Fix #1624 -- Unicode in XML sanitizing causes error
2017-06-05 11:39:52 -05:00
mdipierroandGitHub 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
mdipierroandGitHub 8a741023d8 Merge pull request #1622 from willimoa/enhancement/d3_graph
Enhancement/d3 graph
2017-05-03 23:57:42 -05:00
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 1d77968a06 Merge pull request #1617 from ilvalle/issue_1609
fix open file in py3
2017-04-28 08:12:54 -05:00
mdipierroandGitHub 1842a2e42c Merge pull request #1616 from cccaballero/py3welcome
Fixes in generic layouts for python 3 compatibility
2017-04-28 08:11:42 -05:00
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierroandGitHub 532137bce5 Merge pull request #1594 from BuhtigithuB/improve/translations
Improve french translation and close #1592
2017-03-21 12:24:44 -05:00
mdipierroandGitHub 35f7caa2f0 Merge pull request #1591 from cccaballero/master
Some fixes on ldap_auth
2017-03-21 12:24:05 -05:00
mdipierroandGitHub 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
mdipierroandGitHub 5000c47472 Merge pull request #1568 from amerikan/patch-2
Remove ! from tag
2017-03-21 12:22:16 -05:00
mdipierroandGitHub bf3d53ad96 Merge pull request #1567 from amerikan/patch-3
Removed ! from tag
2017-03-21 12:21:13 -05:00
mdipierroandGitHub 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
mdipierroandGitHub 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
mdipierro f31c9002f7 reverted pydal 16.11 2017-03-21 00:42:02 -05:00
mdipierro 6354fbedf8 commented a test 2017-03-21 00:24:17 -05:00
mdipierro cc11a14ce3 commented a test 2017-03-21 00:21:21 -05:00
mdipierro 2fa9597cd8 added debug info 2017-03-21 00:04:55 -05:00
mdipierro c9d656ea45 added logging, thanks gi0baro 2017-03-20 18:06:23 -05:00
mdipierro 1eec01b830 pydal 17.03 2017-03-20 16:21:05 -05:00
mdipierro c10983b191 Merge branch 'master' of github.com:web2py/web2py 2017-03-20 16:15:31 -05:00
mdipierro 975ab7b923 SQLFORM.grid(showblobs=True) 2017-03-20 16:15:18 -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 MontesandGitHub 48a0683dd1 Removed ! from tag 2017-02-07 10:47:26 -08:00
Erik MontesandGitHub 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
mdipierroandGitHub 16f60d5cff Merge pull request #1565 from kristjanvalur/master
Update compileapp.py
2017-01-30 12:48:37 -06:00
mdipierroandGitHub 930370ee91 Merge pull request #1562 from richboss/dev_urandom
don't write to /dev/urandom on Windows platforms
2017-01-30 12:47:50 -06:00
mdipierro 215abc9e4f download over https for security 2017-01-30 12:46:50 -06:00
Kristján Valur JónssonandGitHub b6e5e16526 Update compileapp.py
The "current" may have been altered by the controller.  This is the case for admin/controllers/shell.py, in callback(), where a new shell environment is created (from gluon/shell)
If this happens, the incorrect "response" is being referenced and the output of the controller is lost.
2017-01-25 20:10:03 +00:00
Richard Boß b2548f5631 don't write to /dev/urandom on Windows platforms 2017-01-22 21:05:22 +01:00
mdipierroandGitHub f14c384d83 Merge pull request #1558 from abastardi/fix-router
Fix router functions check
2017-01-19 09:23:16 -06:00
mdipierroandGitHub 9f186e5d5d Merge pull request #1555 from maxslimmer/master
ccache page fix for disk cache statistics value
2017-01-19 09:23:02 -06:00
mdipierroandGitHub 51dd427b9e Merge pull request #1550 from senolakkas/patch-2
fix css validation error
2017-01-19 09:22:38 -06:00
abastardiandGitHub 7b3fe560ed Update test_router_functions to test extensions 2017-01-14 13:14:04 -05:00
abastardiandGitHub bd526452a8 Fix router functions check
When the router checks whether the requested route matches a function in the
functions list, it does not strip the extension, causing failures for URLs with extensions.
This change strips the extension before comparing to the list of functions.
2017-01-13 15:52:42 -05:00
maxslimmerandGitHub d862da3543 revert part of fix to displaying cache statistics (#1) 2017-01-04 18:08:57 -08:00
maxslimmerandGitHub 0165cfadef get cache.disk statistics value for update and display 2017-01-04 18:03:37 -08:00
maxslimmerandGitHub 46b7716b1d get cache.disk statistics for update 2017-01-04 18:02:23 -08:00
Şenol AKKAŞandGitHub 7e5277030a fix css validation error 2016-12-26 14:21:41 +03:00
mdipierroandGitHub b2e03c9dee Merge pull request #1540 from ilvalle/fix_ipaddress
Replaced ipaddr with ipaddress.py (backported from py3)
2016-12-23 21:58:13 -06:00
mdipierroandGitHub 0644df5283 Merge pull request #1549 from amerikan/patch-1
typo fix
2016-12-23 21:57:56 -06:00
Erik MontesandGitHub 4acd9f8f2b typo fix 2016-12-21 11:17:12 -08:00
mdipierro ce0c5f2d5a Merge branch 'master' of github.com:web2py/web2py 2016-12-20 15:49:44 -06:00
mdipierro 28b0385d9b changes to fabfile 2016-12-20 15:49:30 -06:00
mdipierroandGitHub ac9bccb9a2 Merge pull request #1548 from nextghost/languages
Include Auth and Crud messages in language file updates
2016-12-20 15:42:38 -06:00
mdipierroandGitHub aa5b40528f Merge pull request #1546 from wish7code/master
Fix for #1443 - PR will enable emperor.uwsgi.service on boot
2016-12-20 15:42:10 -06:00
mdipierroandGitHub 96efc7bfce Merge pull request #1539 from smorrison/master
when mkdir of missing app folders, handle broken links properly
2016-12-20 15:40:30 -06:00
mdipierroandGitHub aa584faed6 Merge pull request #1538 from vinyldarkscratch/master
Add web2py componentBegin event
2016-12-20 15:39:55 -06:00
Martin Doucha e7cab3b975 Add Auth and Crud messages when updating language files 2016-12-20 18:54:20 +01:00
wish7 867f93b634 Fix for #1443 - currently emperor.uwsgi.service is not started on boot,
PR will enable emperor.uwsgi.service on boot
(behaviour as in old versions of this script)
2016-12-09 17:21:04 +01:00
mdipierro efff27ffe4 improved fabfile 2016-12-07 23:02:56 -06:00
ilvalle 637579f531 replaced ipaddr with ipaddress.py (backported from py3) 2016-12-03 08:42:35 +01:00
sean 52fe4407b8 when mkdir of missing app folders, handle broken links properly 2016-12-01 10:29:23 -05:00
Vinyl Darkscratch fc0add67b5 Add web2py componentBegin event
Like the “w2p:componentComplete” event I added a while back, this is
another event that I use in my website, and I thought it would be
helpful to share it with others.  This way, you can easily catch when a
web2py component starts loading, and write a handler for whatever
reason (in my case, determining when the user is navigating to a new
page through a smooth loading function piggybacking off of the web2py
component system)
2016-11-29 14:05:17 -08:00
Vinyl Darkscratch 3d2834c81a Merge remote-tracking branch 'web2py/master' 2016-11-29 13:57:46 -08:00
Leonel Câmara a23b264a37 make sudo false again 2016-11-21 19:48:41 +00:00
mdipierro 0b8ecea4dc sudo: required, thanks Leonel 2016-11-21 10:27:16 -06:00
Leonel Câmara 79e256b1d7 require sudo 2016-11-21 15:53:10 +00:00
mdipierroandGitHub 67945c2d5c Merge pull request #1533 from gchiesa/casv3
implemented base support for CASv3
2016-11-21 09:13:41 -06:00
mdipierroandGitHub c600854f4b Merge pull request #1532 from vinyldarkscratch/translations
Fix T.M() not adding to language files
2016-11-21 09:12:42 -06: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
Giuseppe Chiesa 2c70a858f1 implemented base support for CASv3 2016-11-17 13:30:11 +01:00
mdipierroandGitHub ad9ebea900 Merge pull request #1531 from vinyldarkscratch/master
Mobile (iOS) flash dismissal fix
2016-11-14 08:34:00 -06:00
mdipierroandGitHub c72330f2cd Merge pull request #1530 from BMarvi/master
Update extract_mysql_models.py
2016-11-14 08:33:30 -06:00
mdipierroandGitHub 091d9c74b0 Merge pull request #1525 from michele-comitini/confirm_registration_redirect_fix
keep the _next while doing the redirect
2016-11-14 08:27:39 -06:00
mdipierroandGitHub bebdbd9d5e Merge pull request #1519 from matclab/fix/1518
Allow for firstname and lastname in verify_email message
2016-11-14 08:27:10 -06:00
mdipierroandGitHub 05e28ddffd Merge pull request #1516 from pixelbandito/grammar-uppercase-lowercase
Corrected use of 'lowercase' and 'uppercase' to fix #1515
2016-11-14 08:26:44 -06:00
Vinyl Darkscratch e19435dbcf Prevent Firefox from loading the page "javascript:null;" 2016-11-11 18:51:38 -08:00
Vinyl Darkscratch 90ee6f3754 Add three-quote support on markmin 2016-11-11 15:51:01 -08:00
Vinyl Darkscratch d51ea90e18 Code fix to allow adding markmin translations
Playing with this file made me realize that the quotes defining the
strings themselves are added to the regex searches, and that my
previous addition caused invalid syntax.  So, I decided to fix this by
placing the first quote before the markmin decorator.  (Triple quotes
won’t work, however…)
2016-11-11 15:49:28 -08:00
Vinyl Darkscratch 3fed558bdd Merge remote-tracking branch 'web2py/master' into translations 2016-11-11 15:44:21 -08:00
Vinyl Darkscratch 870d9d3e57 Fix .DS_Store ignore 2016-11-11 13:52:43 -08:00
Vinyl DarkscratchandGitHub 0a07af55f9 Replace "#" link with "javascript:null;" 2016-11-11 03:31:10 -08:00
Vinyl DarkscratchandGitHub f7adfbde76 Replace "#" link with "javascript:null;" 2016-11-11 03:30:50 -08:00
Vinyl DarkscratchandGitHub 75c1d80824 Replace "#" link with "javascript:null;" 2016-11-11 03:30:16 -08:00
MarviandGitHub c64a9192d4 Update extract_mysql_models.py 2016-11-11 10:12:11 +01:00
Vinyl Darkscratch 8e79e49ae3 Mobile (iOS) fix for dismissing flashes
iOS devices don’t like listening to clicks on most objects.  They
typically prefer a and button objects.  This fix replaces the
#closeflash span with a link to “#” instead (while also inheriting
text-decoration and color styling), so that mobile (iOS) devices will
allow you to close the flash.
2016-11-10 01:19:06 -08:00
Vinyl Darkscratch 46ce04355f Merge remote-tracking branch 'web2py/master' 2016-11-10 01:14:40 -08: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
Michele Comitini 6b1225da02 keep the _next while doing the redirect 2016-11-04 09:59:49 +01:00
Michele ComitiniandGitHub 4ea31820aa Merge pull request #1520 from michele-comitini/wrong_serializers_import
fix wrong import in default controller of admin app
2016-11-03 12:00:15 +01:00
Michele Comitini 8e1630843a fix wrong import in default controller of admin app 2016-11-02 00:41:40 +01:00
Mathieu Clabaut 2d4817841f Allow for firstname and lastname in verify_email message 2016-11-01 11:31:01 +01:00
Chris Garcia 4226b6d0e1 Corrected use of 'lowercase' and 'uppercase' to fix #1515 2016-10-28 15:23:18 -05:00
Vinyl Darkscratch 3e2b8f89aa Merge remote-tracking branch 'web2py/master' into translations 2016-10-10 20:45:42 -07:00
98 changed files with 6584 additions and 4168 deletions
+1
View File
@@ -13,6 +13,7 @@
*.orig *.orig
Thumbs.db Thumbs.db
.DS_Store .DS_Store
*.DS_Store
index.yaml index.yaml
routes.py routes.py
logging.conf logging.conf
+13 -2
View File
@@ -1,13 +1,21 @@
language: python language: python
sudo: false sudo: required
cache: pip cache: pip
dist: "trusty"
python: python:
- '2.7' - '2.7'
- 'pypy'
- '3.5' - '3.5'
- '3.6'
- 'pypy-5.3.1'
- 'pypy3.5-5.7.1-beta'
matrix:
allow_failures:
- python: 'pypy3.5-5.7.1-beta'
install: install:
- pip install -e . - pip install -e .
@@ -32,3 +40,6 @@ notifications:
addons: addons:
postgresql: "9.4" postgresql: "9.4"
apt:
packages:
- postgresql-9.4-postgis-2.3
+38 -7
View File
@@ -1,8 +1,29 @@
## 2.15.0b1
## 2.15.x - dropped support for python 2.6
- web2py does not support python 2.6 anymore - dropped web shell
- py3.5 syntax compatible (see #1353 for details) - experimental python 3 support
- dropped web shell from admin - experimental authapi for service login
- allow ajax file uploads
- more tests
- more pep8 compliance
- 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
- scheduler new feature: you can now specify intervals with cron - scheduler new feature: you can now specify intervals with cron
- gluon/* removed from sys.path. Applications relying on statements like e.g. - gluon/* removed from sys.path. Applications relying on statements like e.g.
"from storage import Storage" "from storage import Storage"
@@ -10,8 +31,18 @@
"from gluon.storage import Storage" "from gluon.storage import Storage"
- tests can only be run with the usual web2py.py --run_system_tests OR with - tests can only be run with the usual web2py.py --run_system_tests OR with
python -m unittest -v gluon.tests on the root dir python -m unittest -v gluon.tests on the root dir
- updated pymysql driver - 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
new pymysql driver
## 2.14.6 ## 2.14.6
+2 -2
View File
@@ -32,7 +32,7 @@ update:
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.14.6-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.15.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
@@ -54,7 +54,7 @@ src:
### build web2py_src.zip ### build web2py_src.zip
echo '' > NEWINSTALL echo '' > NEWINSTALL
mv web2py_src.zip web2py_src_old.zip | echo 'no old' mv web2py_src.zip web2py_src_old.zip | echo 'no old'
cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py
mdp: mdp:
make src make src
+1 -1
View File
@@ -1 +1 @@
Version 2.14.6-stable+timestamp.2016.05.09.19.18.48 Version 2.15.2-stable+timestamp.2017.07.19.01.21.31
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment # ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0]))) ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict): if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses'] disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0) _cellborder=0, _cellspacing=0)
).xml() ).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(): def manage():
tables = manager_action['tables'] tables = manager_action['tables']
if isinstance(tables[0], str): if isinstance(tables[0], str):
@@ -699,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_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) ul_main.append(ul_t)
return ul_main 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)
+2 -1
View File
@@ -6,6 +6,7 @@ import gluon.validators
import code import code
from gluon.debug import communicate, web_debugger, dbg_debugger from gluon.debug import communicate, web_debugger, dbg_debugger
from gluon._compat import thread from gluon._compat import thread
from gluon.fileutils import open_file
import pydoc import pydoc
@@ -54,7 +55,7 @@ def interact():
if filename: if filename:
# prevent IOError 2 on some circuntances (EAFP instead of os.access) # prevent IOError 2 on some circuntances (EAFP instead of os.access)
try: try:
lines = open(filename).readlines() lines = open_file(filename, 'r').readlines()
except: except:
lines = "" lines = ""
lines = dict([(i + 1, l) for (i, l) in enumerate( lines = dict([(i + 1, l) for (i, l) in enumerate(
+5 -2
View File
@@ -16,6 +16,7 @@ from gluon.tools import Config
from gluon.compileapp import find_exposed_functions from gluon.compileapp import find_exposed_functions
from glob import glob from glob import glob
from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native
import gluon.rewrite
import shutil import shutil
import platform import platform
@@ -249,6 +250,7 @@ def site():
db.app.insert(name=appname, owner=auth.user.id) db.app.insert(name=appname, owner=auth.user.id)
log_progress(appname) log_progress(appname)
session.flash = T('new application "%s" created', appname) session.flash = T('new application "%s" created', appname)
gluon.rewrite.load()
redirect(URL('design', args=appname)) redirect(URL('design', args=appname))
else: else:
session.flash = \ session.flash = \
@@ -266,6 +268,7 @@ def site():
new_repo = git.Repo.clone_from(form_update.vars.url, target) new_repo = git.Repo.clone_from(form_update.vars.url, target)
session.flash = T('new application "%s" imported', session.flash = T('new application "%s" imported',
form_update.vars.name) form_update.vars.name)
gluon.rewrite.load()
except git.GitCommandError as err: except git.GitCommandError as err:
session.flash = T('Invalid git repository specified.') session.flash = T('Invalid git repository specified.')
redirect(URL(r=request)) redirect(URL(r=request))
@@ -302,6 +305,7 @@ def site():
log_progress(appname) log_progress(appname)
session.flash = T(msg, dict(appname=appname, session.flash = T(msg, dict(appname=appname,
digest=md5_hash(installed))) digest=md5_hash(installed)))
gluon.rewrite.load()
else: else:
msg = 'unable to install application "%(appname)s"' msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name)) session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -1861,7 +1865,6 @@ def user():
def reload_routes(): def reload_routes():
""" Reload routes.py """ """ Reload routes.py """
import gluon.rewrite
gluon.rewrite.load() gluon.rewrite.load()
redirect(URL('site')) redirect(URL('site'))
@@ -1960,7 +1963,7 @@ def git_push():
def plugins(): def plugins():
app = request.args(0) app = request.args(0)
from serializers import loads_json from gluon.serializers import loads_json
if not session.plugins: if not session.plugins:
try: try:
rawlist = urlopen("http://www.web2pyslices.com/" + rawlist = urlopen("http://www.web2pyslices.com/" +
+138 -137
View File
@@ -21,18 +21,19 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', '**%(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)', '**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', '@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)', '``**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: %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', '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', 'Abort': 'Abort',
'About': 'à propos', 'About': 'À propos',
'About application': "A propos de l'application", 'About application': "À propos de l'application",
'Accept Terms': 'Termes acceptés', 'Accept Terms': 'Termes acceptés',
'Add breakpoint': 'Ajouter une interruption', 'Add breakpoint': 'Ajouter une interruption',
'additional code for your application': 'code supplémentaire pour votre application', 'additional code for your application': 'code supplémentaire pour votre application',
'Additional code for your application': 'Code additionnel 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 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 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', '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 language': "Language de l'admin",
'Admin versioning page': 'Admin versioning page', 'Admin versioning page': 'Admin versioning page',
'administrative interface': "interface d'administration", '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 c’est produite, s’il vous plait [[reload %s]] la page', 'An error occured, please [[reload %s]] the page': 'Une erreur c’est produite, s’il vous plait [[reload %s]] la page',
'and rename it (required):': 'et renommez-la (obligatoire):', 'and rename it (required):': 'et renommez-la (obligatoire):',
'and rename it:': 'et renommez-le:', '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 is compiled and cannot be designed': "l'application est compilée et ne peut être modifiée",
'Application name:': "Nom de l'application:", 'Application name:': "Nom de l'application:",
'Application updated via git pull': 'Application updated via git pull', 'Application updated via git pull': 'Application updated via git pull',
'are not used': 'are not used', 'are not used': 'ne sont pas utilisé',
'are not used yet': 'are not used yet', '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 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 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 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 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 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', 'arguments': 'arguments',
'at char %s': 'at char %s', 'at char %s': 'at char %s',
'at line %s': 'at line %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: 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!", 'ATTENTION: you cannot edit the running application!': "ATTENTION: vous ne pouvez pas modifier l'application qui tourne!",
'Autocomplete Python Code': 'Autocomplete Python Code', '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', 'Available Databases and Tables': 'Available Databases and Tables',
'back': 'retour', 'back': 'retour',
'Back to the plugins list': 'Retour à la liste de plugins', 'Back to the plugins list': 'Retour à la liste de plugins',
'Back to wizard': 'Back to wizard', 'Back to wizard': 'Back to wizard',
'Basics': 'Basics', 'Basics': 'Basics',
'Begin': 'Begin', 'Begin': 'Début',
'breakpoint': 'breakpoint', 'breakpoint': 'breakpoint',
'Breakpoints': 'Breakpoints', 'Breakpoints': 'Breakpoints',
'breakpoints': 'breakpoints', 'breakpoints': 'breakpoints',
@@ -92,7 +93,7 @@
'Cache Keys': 'Cache Keys', 'Cache Keys': 'Cache Keys',
'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyés', 'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyés',
'can be a git repo': 'can be a git repo', 'can be a git repo': 'can be a git repo',
'Cancel': 'Retour', 'Cancel': 'Annuler',
'Cannot be empty': 'Ne peut pas être vide', '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. 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:', '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 admin password': 'Changer le mot de passe admin',
'change editor settings': 'change editor settings', 'change editor settings': 'change editor settings',
'Changelog': 'Changelog', 'Changelog': 'Changelog',
'check all': 'tout vérifier ', 'check all': 'tout sélectionner',
'Check for upgrades': 'Vérifier les mises à jour', 'Check for upgrades': 'Vérifier les mises à jour',
'Check to delete': 'Cocher pour supprimer', 'Check to delete': 'Cocher pour supprimer',
'Checking for upgrades...': 'Vérification des mises à jour ... ', 'Checking for upgrades...': 'Vérification des mises à jour ... ',
'Clean': 'nettoyer', 'Clean': 'nettoyer',
'Clear': 'Clear', 'Clear': 'Effacer',
'Clear CACHE?': 'Clear CACHE?', 'Clear CACHE?': 'Effacer le CACHE?',
'Clear DISK': 'Clear DISK', 'Clear DISK': 'Effacer le DISQUE',
'Clear RAM': 'Clear RAM', 'Clear RAM': 'Effacer la RAM',
'Click row to expand traceback': 'Click row to expand traceback', 'Click row to expand traceback': 'Click row to expand traceback',
'Click row to view a ticket': 'Click row to view a ticket', 'Click row to view a ticket': 'Click row to view a ticket',
'click to check for upgrades': 'Cliquez pour vérifier les mises jour', 'click to check for upgrades': 'Cliquez pour vérifier les mises jour',
'code': 'code', 'code': 'code',
'Code listing': 'Code listing', 'Code listing': 'Code listing',
'collapse/expand all': 'tout réduire/agrandir', 'collapse/expand all': 'tout réduire/agrandir',
'Command': 'Command', 'Command': 'Commande',
'Comment:': 'Comment:', 'Comment:': 'Commentaire:',
'Commit': 'Commit', 'Commit': 'Valider',
'Commit form': 'Commit form', 'Commit form': 'Valider le formulaire',
'Committed files': 'Committed files', 'Committed files': 'Fichiers validés',
'Compile': 'compiler', 'Compile': 'compiler',
'Compile (all or nothing)': 'Compile (all or nothing)', 'Compile (all or nothing)': 'Compile (all or nothing)',
'Compile (skip failed views)': 'Compile (skip failed views)', 'Compile (skip failed views)': 'Compile (skip failed views)',
'compiled application removed': 'application compilée enlevée', 'compiled application removed': 'application compilée enlevée',
'Condition': 'Condition', 'Condition': 'Condition',
'continue': 'continue', 'continue': 'continuer',
'Controllers': 'Contrôleurs', 'Controllers': 'Contrôleurs',
'controllers': 'contrôleurs', 'controllers': 'contrôleurs',
'Count': 'Count', 'Count': 'Compte',
'Create': 'Créer', 'Create': 'Créer',
'create file with filename:': 'créer un fichier avec nom de fichier:', 'create file with filename:': 'créer un fichier avec nom de fichier:',
'create new application:': 'créer une nouvelle application:', 'create new application:': 'créer une nouvelle application:',
'Create new simple application': 'Créer une nouvelle application', 'Create new simple application': 'Créer une nouvelle application',
'Create/Upload': 'Create/Upload', 'Create/Upload': 'Create/Upload',
'created by': 'créé par', 'created by': 'créé par',
'Created by:': 'Created by:', 'Created by:': 'Créé par:',
'Created On': 'Created On', 'Created On': 'Créé le',
'Created on:': 'Created on:', 'Created on:': 'Créé le:',
'crontab': 'crontab', 'crontab': 'crontab',
'Current request': 'Requête actuelle', 'Current request': 'Requête actuelle',
'Current response': 'Réponse actuelle', 'Current response': 'Réponse actuelle',
'Current session': 'Session en cours', 'Current session': 'Session en cours',
'currently running': 'tourne actuellement', 'currently running': 'tourne actuellement',
'currently saved or': 'actuellement enregistré ou', 'currently saved or': 'actuellement enregistré ou',
'data uploaded': 'données chargées', 'data uploaded': 'données téléversées',
'Database': 'Database', 'Database': 'Base de données',
'database': 'base de données', 'database': 'base de données',
'Database %s select': 'Database %s select', 'Database %s select': 'Database %s select',
'database %s select': 'base de données %s sélectionner', 'database %s select': 'base de données %s sélectionner',
'Database administration': 'Database administration', 'Database administration': 'Administration base de données',
'database administration': 'administration base de données', 'database administration': 'administration base de données',
'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Database Administration (appadmin)': 'Database Administration (appadmin)',
'Date and Time': 'Date et heure', 'Date and Time': 'Date et heure',
'db': 'bdd', 'db': 'bd',
'Debug': 'Debug', 'Debug': 'Debug',
'defines tables': 'définit les tables', 'defines tables': 'définit les tables',
'Delete': 'Supprimer', '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 this file (you will be asked to confirm deletion)': 'Supprimer ce fichier (on vous demandera de confirmer la suppression)',
'Delete:': 'Supprimer:', 'Delete:': 'Supprimer:',
'deleted after first hit': 'deleted after first hit', 'deleted after first hit': 'deleted after first hit',
'Demo': 'Demo', 'Demo': 'Démo',
'Deploy': 'Déployer', 'Deploy': 'Déployer',
'Deploy on Google App Engine': 'Déployer sur Google App Engine', 'Deploy on Google App Engine': 'Déployer sur Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift', 'Deploy to OpenShift': 'Deploy to OpenShift',
@@ -176,22 +177,22 @@
'Description:': 'Description:', 'Description:': 'Description:',
'design': 'conception', 'design': 'conception',
'Detailed traceback description': 'Detailed traceback description', 'Detailed traceback description': 'Detailed traceback description',
'details': 'details', 'details': 'détails',
'direction: ltr': 'direction: ltr', 'direction: ltr': 'direction: ltr',
'directory not found': 'directory not found', 'directory not found': 'directory not found',
'Disable': 'Disable', 'Disable': 'Désactiver',
'Disabled': 'Disabled', 'Disabled': 'Désactivé',
'disabled in demo mode': 'disabled in demo mode', 'disabled in demo mode': 'disabled in demo mode',
'disabled in GAE mode': 'disabled in GAE mode', 'disabled in GAE mode': 'disabled in GAE mode',
'disabled in multi user mode': 'disabled in multi user mode', 'disabled in multi user mode': 'disabled in multi user mode',
'DISK': 'DISK', 'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys', '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.', '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', 'Display line numbers': 'Display line numbers',
'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.',
'docs': 'docs', 'docs': 'documents',
'Docs': 'Docs', 'Docs': 'Documents',
'done!': 'fait!', 'done!': 'fait!',
'Downgrade': 'Downgrade', 'Downgrade': 'Downgrade',
'Download .w2p': 'Download .w2p', 'Download .w2p': 'Download .w2p',
@@ -201,43 +202,43 @@
'download plugins': 'télécharger plugins', 'download plugins': 'télécharger plugins',
'Download plugins from repository': 'Download plugins from repository', 'Download plugins from repository': 'Download plugins from repository',
'EDIT': 'MODIFIER', 'EDIT': 'MODIFIER',
'Edit': 'modifier', 'Edit': 'Modifier',
'edit all': 'edit all', 'edit all': 'tout modifier',
'Edit application': "Modifier l'application", 'Edit application': "Modifier l'application",
'edit controller': 'modifier contrôleur', 'edit controller': 'modifier contrôleur',
'edit controller:': 'edit controller:', 'edit controller:': 'modifier le contrôleur:',
'Edit current record': 'Modifier cette entrée', 'Edit current record': 'Modifier cet enregistrement',
'edit views:': 'modifier vues:', 'edit views:': 'modifier les vues:',
'Editing %s': 'Editing %s', 'Editing %s': 'Modifier %s',
'Editing file': 'Modifier le fichier', 'Editing file': 'Modifier le fichier',
'Editing file "%s"': 'Modifier le fichier "% s" ', 'Editing file "%s"': 'Modifier le fichier "% s" ',
'Editing Language file': 'Modifier le fichier de langue', 'Editing Language file': 'Modifier le fichier de langue',
'Editing Plural Forms File': 'Editing Plural Forms File', 'Editing Plural Forms File': 'Modifier le fichier du formulaire pluriel',
'Editor': 'Editor', 'Editor': 'Éditeur',
'Email Address': 'Email Address', 'Email Address': 'Adresse courriel',
'Enable': 'Enable', 'Enable': 'Activer',
'Enable Close-Tag': 'Enable Close-Tag', 'Enable Close-Tag': 'Enable Close-Tag',
'Enable Code Folding': 'Enable Code Folding', 'Enable Code Folding': 'Enable Code Folding',
'Enterprise Web Framework': 'Enterprise Web Framework', 'Enterprise Web Framework': 'Enterprise Web Framework',
'Error': 'Error', 'Error': 'Error',
'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"', 'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"',
'Error snapshot': 'Error snapshot', 'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket', 'Error ticket': "Billet d'erreur",
'Errors': 'erreurs', 'Errors': 'Erreurs',
'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', 'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s',
'Exception %s': 'Exception %s', 'Exception %s': 'Exception %s',
'Exception instance attributes': "Attributs d'instance Exception", 'Exception instance attributes': "Attributs d'instance Exception",
'Exit Fullscreen': 'Exit Fullscreen', 'Exit Fullscreen': 'Exit Fullscreen',
'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)',
'export as csv file': 'export au format CSV', 'export as csv file': 'export au format CSV',
'Exports:': 'Exports:', 'Exports:': 'Exportions:',
'exposes': 'expose', 'exposes': 'expose',
'exposes:': 'expose:', 'exposes:': 'expose:',
'extends': 'étend', 'extends': 'étend',
'failed to compile file because:': 'failed to compile file because:', 'failed to compile file because:': 'failed to compile file because:',
'failed to reload module': 'impossible de recharger le module', 'failed to reload module': 'impossible de recharger le module',
'failed to reload module because:': 'impossible de recharger le module car:', '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" created': 'fichier "%(filename)s" créé',
'file "%(filename)s" deleted': 'fichier "%(filename)s" supprimé', 'file "%(filename)s" deleted': 'fichier "%(filename)s" supprimé',
'file "%(filename)s" uploaded': 'fichier "%(filename)s" chargé', 'file "%(filename)s" uploaded': 'fichier "%(filename)s" chargé',
@@ -247,19 +248,19 @@
'file not found': 'file not found', 'file not found': 'file not found',
'file saved on %(time)s': 'fichier enregistré le %(time)s', 'file saved on %(time)s': 'fichier enregistré le %(time)s',
'file saved on %s': 'fichier enregistré le %s', 'file saved on %s': 'fichier enregistré le %s',
'filename': 'filename', 'filename': 'nom de fichier',
'Filename': 'Filename', 'Filename': 'Nom de fichier',
'Files added': 'Files added', 'Files added': 'Fichiers ajoutés',
'filter': 'filtre', 'filter': 'filtre',
'Find Next': 'Find Next', 'Find Next': 'Trouver les suivants',
'Find Previous': 'Find Previous', 'Find Previous': 'Trouver les précédents',
'Form has errors': 'Form has errors', 'Form has errors': 'Le formulaire comporte des erreurs',
'Frames': 'Frames', 'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraîneront des tests [passed] .', 'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraîneront des tests [passed] .',
'GAE Email': 'GAE Email', 'GAE Email': 'GAE Email',
'GAE Output': 'GAE Output', 'GAE Output': 'GAE Output',
'GAE Password': 'GAE Password', 'GAE Password': 'Mot de passe GAE',
'Generate': 'Generate', 'Generate': 'Générer',
'Git Pull': 'Git Pull', 'Git Pull': 'Git Pull',
'Git Push': 'Git Push', 'Git Push': 'Git Push',
'Globals##debug': 'Globals##debug', 'Globals##debug': 'Globals##debug',
@@ -267,15 +268,15 @@
'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', 'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface',
'Google Application Id': 'Google Application Id', 'Google Application Id': 'Google Application Id',
'Goto': 'Goto', 'Goto': 'Goto',
'graph model': 'graph model', 'graph model': 'représentation graphique du modèle',
'Graph Model': 'Graph Model', 'Graph Model': 'Représentation graphique du modèle',
'Help': 'aide', 'Help': 'aide',
'here': 'here', 'here': 'ici',
'Hide/Show Translated strings': 'Hide/Show Translated strings', 'Hide/Show Translated strings': 'Hide/Show Translated strings',
'Highlight current line': 'Highlight current line', '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)})', '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', 'Hits': 'Hits',
'Home': 'Home', 'Home': 'Accueil',
'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true',
'htmledit': 'edition html', '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', '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.\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 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.', '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', '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.', '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', 'includes': 'inclus',
@@ -311,14 +312,14 @@
'Invalid request': 'Invalid request', 'Invalid request': 'Invalid request',
'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)',
'invalid ticket': 'ticket non valide', 'invalid ticket': 'ticket non valide',
'Key': 'Key', 'Key': 'Clé',
'Keyboard shortcuts': 'Keyboard shortcuts', 'Keyboard shortcuts': 'Keyboard shortcuts',
'kill process': 'kill process', 'kill process': 'kill process',
'language file "%(filename)s" created/updated': 'fichier de langue "%(filename)s" créé/mis à jour', '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 ', 'Language files (static strings) updated': 'Fichiers de langue (chaînes statiques) mis à jour ',
'languages': 'langues', 'languages': 'langues',
'Languages': 'Langues', 'Languages': 'Langues',
'Last Revision': 'Last Revision', 'Last Revision': 'Dernière révision',
'Last saved on:': 'Dernière sauvegarde le:', 'Last saved on:': 'Dernière sauvegarde le:',
'License for': 'Licence pour', 'License for': 'Licence pour',
'License:': 'License:', 'License:': 'License:',
@@ -326,8 +327,8 @@
'Line number': 'Line number', 'Line number': 'Line number',
'lists by exception': 'lists by exception', 'lists by exception': 'lists by exception',
'lists by ticket': 'lists by ticket', 'lists by ticket': 'lists by ticket',
'Loading...': 'Loading...', 'Loading...': 'Chargement...',
'loading...': 'Chargement ...', 'loading...': 'chargement ...',
'Local Apps': 'Local Apps', 'Local Apps': 'Local Apps',
'locals': 'locals', 'locals': 'locals',
'Locals##debug': 'Locals##debug', 'Locals##debug': 'Locals##debug',
@@ -337,7 +338,7 @@
'Login to the Administrative Interface': "Se connecter à l'interface d'administration", 'Login to the Administrative Interface': "Se connecter à l'interface d'administration",
'Login/Register': 'Login/Register', 'Login/Register': 'Login/Register',
'Logout': 'déconnexion', 'Logout': 'déconnexion',
'lost password': 'lost password', 'lost password': 'mot de passe perdu',
'Main Menu': 'Main Menu', 'Main Menu': 'Main Menu',
'Manage': 'Manage', 'Manage': 'Manage',
'Manage %(action)s': 'Manage %(action)s', 'Manage %(action)s': 'Manage %(action)s',
@@ -350,7 +351,7 @@
'merge': 'fusionner', 'merge': 'fusionner',
'Models': 'Modèles', 'Models': 'Modèles',
'models': 'modèles', 'models': 'modèles',
'Modified On': 'Modified On', 'Modified On': 'Modifié le',
'Modules': 'Modules', 'Modules': 'Modules',
'modules': 'modules', 'modules': 'modules',
'Multi User Mode': 'Multi User Mode', 'Multi User Mode': 'Multi User Mode',
@@ -363,8 +364,8 @@
'New Record': 'Nouvelle Entrée', 'New Record': 'Nouvelle Entrée',
'new record inserted': 'nouvelle entrée insérée', 'new record inserted': 'nouvelle entrée insérée',
'New simple application': 'Nouvelle application simple', 'New simple application': 'Nouvelle application simple',
'next': 'next', 'next': 'suivant',
'next %s rows': 'next %s rows', 'next %s rows': '%s lignes suivantes',
'next 100 rows': '100 lignes suivantes', 'next 100 rows': '100 lignes suivantes',
'NO': 'NON', 'NO': 'NON',
'no changes': 'no changes', 'no changes': 'no changes',
@@ -374,8 +375,8 @@
'no package selected': 'no package selected', 'no package selected': 'no package selected',
'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'no permission to uninstall "%s"': 'no permission to uninstall "%s"',
'Node:': 'Node:', 'Node:': 'Node:',
'Not Authorized': 'Not Authorized', 'Not Authorized': 'Pas autorisé',
'Not supported': 'Not supported', '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.', '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**', '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.", "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', 'Peeking at file': 'Jeter un oeil au fichier',
'Permission': 'Permission', 'Permission': 'Permission',
'Permissions': 'Permissions', 'Permissions': 'Permissions',
'Please': 'Please', 'Please': 'SVP',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" supprimé', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" supprimé',
'Plugin "%s" in application': 'Plugin "%s" dans l\'application', 'Plugin "%s" in application': 'Plugin "%s" dans l\'application',
'plugin not specified': 'plugin not specified', 'plugin not specified': 'plugin not specified',
'Plugin page': 'Plugin page', 'Plugin page': 'Plugin page',
'plugins': 'plugins', 'plugins': 'plugiciels',
'Plugins': 'Plugins', 'Plugins': 'Plugiciels',
'Plural Form #%s': 'Plural Form #%s', 'Plural Form #%s': 'Plural Form #%s',
'Plural-Forms:': 'Plural-Forms:', 'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Propulsé par', 'Powered by': 'Propulsé par',
@@ -423,7 +424,7 @@
'Pull': 'Pull', '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 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.', '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.', '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', 'pygraphviz library not found': 'pygraphviz library not found',
'PythonAnywhere Apps': 'PythonAnywhere Apps', 'PythonAnywhere Apps': 'PythonAnywhere Apps',
@@ -433,9 +434,9 @@
'RAM Cache Keys': 'RAM Cache Keys', 'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared', '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.', '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', 'Rapid Search': 'Recherche rapide',
'Record': 'Record', 'Record': 'Enregistrement',
'record': 'entrée', 'record': 'enregistrement',
'record does not exist': "l'entrée n'existe pas", 'record does not exist': "l'entrée n'existe pas",
'record id': 'id entrée', 'record id': 'id entrée',
'Record id': 'Record id', 'Record id': 'Record id',
@@ -444,24 +445,24 @@
'Reload routes': 'Reload routes', 'Reload routes': 'Reload routes',
'Remove compiled': 'retirer compilé', 'Remove compiled': 'retirer compilé',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Replace', 'Replace': 'Remplacer',
'Replace All': 'Replace All', 'Replace All': 'Tout remplacer',
'Repository (%s)': 'Repository (%s)', 'Repository (%s)': 'Repository (%s)',
'request': 'request', 'request': 'request',
'requires distutils, but not installed': 'requires distutils, but not installed', 'requires distutils, but not installed': 'requires distutils, but not installed',
'requires python-git, but not installed': 'requires python-git, but not installed', 'requires python-git, but not installed': 'requires python-git, but not installed',
'Resolve Conflict file': 'Résoudre les conflits de fichiers', 'Resolve Conflict file': 'Résoudre les conflits de fichiers',
'response': 'response', 'response': 'response',
'restart': 'restart', 'restart': 'redémarrer',
'restore': 'restaurer', 'restore': 'restaurer',
'return': 'return', 'return': 'retour',
'Revert': 'Revert', 'Revert': "Revenir vers l'arrière",
'revert': 'revenir', 'revert': "revenir vers l'arrière",
'reverted to revision %s': 'reverted to revision %s', 'reverted to revision %s': 'reverted to revision %s',
'Revision %s': 'Revision %s', 'Revision %s': 'Révision %s',
'Revision:': 'Revision:', 'Revision:': 'Révision:',
'Role': 'Role', 'Role': 'Rôle',
'Roles': 'Roles', 'Roles': 'Rôles',
'Rows in Table': 'Rows in Table', 'Rows in Table': 'Rows in Table',
'Rows in table': 'Lignes de la table', 'Rows in table': 'Lignes de la table',
'Rows selected': 'Lignes sélectionnées', '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': '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')", "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', 'Running on %s': 'Running on %s',
'Save': 'Enregistrer', 'Save': 'Sauvegarder',
'save': 'sauver', 'save': 'sauvegarder',
'Save file:': 'Save file:', 'Save file:': 'Sauvegarder le fichier:',
'Save file: %s': 'Save file: %s', 'Save file: %s': 'Sauvegarder le fichier : %s',
'Save model as...': 'Save model as...', 'Save model as...': 'Sauvegarder le modèle sous...',
'Save via Ajax': 'Save via Ajax', 'Save via Ajax': 'Sauvegarder via Ajax',
'Saved file hash:': 'Hash du Fichier enregistré:', 'Saved file hash:': 'Hash du Fichier enregistré:',
'Screenshot %s': 'Screenshot %s', 'Screenshot %s': "Capture d'écran %s",
'Search': 'Search', 'Search': 'Rechercher',
'Searching: **%s** %%{file}': 'Searching: **%s** %%{file}', 'Searching: **%s** %%{file}': 'Searching: **%s** %%{file}',
'Select Files to Package': 'Select Files to Package', 'Select Files to Package': 'Select Files to Package',
'selected': 'sélectionnés', 'selected': 'sélectionnés',
@@ -491,34 +492,34 @@
'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', 'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found',
'Singular Form': 'Singular Form', 'Singular Form': 'Singular Form',
'Site': 'Site', 'Site': 'Site',
'Size of cache:': 'Size of cache:', 'Size of cache:': 'Taille de la mémoire cache:',
'skip to generate': 'skip to generate', 'skip to generate': 'skip to generate',
'some files could not be removed': 'certains fichiers ne peuvent pas être supprimés', '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', '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', 'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed',
'source : db': 'source : db', 'source : db': 'source : db',
'source : filesystem': 'source : filesystem', 'source : filesystem': 'source : système de fichier',
'Start a new app': 'Start a new app', 'Start a new app': 'Commencer une nouvelle application',
'Start searching': 'Start searching', 'Start searching': 'Débuté la recherche',
'Start wizard': "Démarrer l'assistant", 'Start wizard': "Démarrer l'assistant",
'state': 'état', 'state': 'état',
'Static': 'Static', 'Static': 'Statique',
'static': 'statiques', 'static': 'statique',
'Static files': 'Fichiers statiques', 'Static files': 'Fichiers statiques',
'Statistics': 'Statistics', 'Statistics': 'Statistics',
'Step': 'Step', 'Step': 'Step',
'step': 'step', 'step': 'step',
'stop': 'stop', 'stop': 'arrêt',
'submit': 'envoyer', 'submit': 'envoyer',
'Submit': 'Submit', 'Submit': 'Envoyer',
'successful': 'successful', 'successful': 'réussi',
'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet? ', 'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet?',
'switch to : db': 'switch to : db', 'switch to : db': 'transférer dans : db',
'switch to : filesystem': 'switch to : filesystem', 'switch to : filesystem': 'switch to : filesystem',
'Tab width (# characters)': 'Tab width (# characters)', 'Tab width (# characters)': 'Tab width (# characters)',
'table': 'table', 'table': 'table',
'Table': 'Table', 'Table': 'Table',
'Temporary': 'Temporary', 'Temporary': 'Temporaire',
'test': 'tester', 'test': 'tester',
'Testing application': "Test de l'application", '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.', '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 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': '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', '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 controllers': "Il n'y a pas de contrôleurs",
'There are no models': "Il n'y a pas de modèles", '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 modules': "Il n'y a pas de modules",
'There are no plugins': "Il n'y a pas de plugins", '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 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 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", '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', '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 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 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".', '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': 'Billet',
'Ticket ID': 'Ticket ID', 'Ticket ID': 'Identifiant du Billet',
'Ticket Missing': 'Ticket Missing', 'Ticket Missing': 'Billet manquant',
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'TM': 'MD', 'TM': 'MD',
'to previous version.': 'à la version précédente.', 'to previous version.': 'à la version précédente.',
@@ -569,8 +570,8 @@
'Translation strings for the application': "Chaînes de traduction pour l'application", 'Translation strings for the application': "Chaînes de traduction pour l'application",
'try': 'essayer', 'try': 'essayer',
'try something like': 'essayez quelque chose comme', 'try something like': 'essayez quelque chose comme',
'Try the mobile interface': 'Try the mobile interface', 'Try the mobile interface': "Essayer l'interface pour mobile",
'try view': 'try view', '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 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.', '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', 'Unable to check for upgrades': 'Impossible de vérifier les mises à jour',
@@ -595,25 +596,25 @@
'update': 'mettre à jour', 'update': 'mettre à jour',
'update all languages': 'mettre à jour toutes les langues', 'update all languages': 'mettre à jour toutes les langues',
'Update:': 'Mise à jour:', 'Update:': 'Mise à jour:',
'Upgrade': 'Upgrade', 'Upgrade': 'Mese à jour de version',
'upgrade now': 'mettre à jour maintenant', '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', 'upgrade web2py now': 'mettre à jour web2py maintenant',
'upload': 'charger', 'upload': 'téléversé',
'Upload': 'Upload', 'Upload': 'Téléversé',
'Upload & install packed application': "Charger & installer l'application empaquetée", 'Upload & install packed application': "Charger & installer l'application empaquetée",
'Upload a package:': 'Charger un paquet:', 'Upload a package:': 'Téléverser un paquet:',
'Upload and install packed application': 'Upload and install packed application', 'Upload and install packed application': 'Téléversement et installation du paquet applicatif',
'upload application:': "charger l'application:", 'upload application:': "téléverser l'application:",
'Upload existing application': 'Charger une application existante', 'Upload existing application': 'Téléverser une application existante',
'upload file:': 'charger le fichier:', 'upload file:': 'téléverser le fichier:',
'upload plugin file:': 'charger fichier plugin:', '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 (...)&(...) 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:', 'Use an url:': 'Utiliser une url:',
'user': 'utilisateur', 'user': 'utilisateur',
'User': 'User', 'User': 'Utilisateur',
'Username': 'Username', 'Username': "Nom d'utilisateur",
'Users': 'Users', '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.', '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', 'variables': 'variables',
'Version': 'Version', 'Version': 'Version',
@@ -625,9 +626,9 @@
'Warning!': 'Warning!', 'Warning!': 'Warning!',
'WARNING:': 'WARNING:', 'WARNING:': 'WARNING:',
'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:',
'Web Framework': 'Framework Web', 'Web Framework': 'Cadre Web',
'web2py Admin Password': 'web2py Admin Password', 'web2py Admin Password': 'Mot de passe admin web2py',
'web2py apps to deploy': 'web2py apps to deploy', 'web2py apps to deploy': 'applications web2py à deployer',
'web2py Debugger': 'web2py Debugger', 'web2py Debugger': 'web2py Debugger',
'web2py downgrade': 'web2py downgrade', 'web2py downgrade': 'web2py downgrade',
'web2py is up to date': 'web2py est à jour', 'web2py is up to date': 'web2py est à jour',
@@ -635,10 +636,10 @@
'web2py Recent Tweets': 'Tweets récents sur web2py ', 'web2py Recent Tweets': 'Tweets récents sur web2py ',
'web2py upgrade': 'web2py upgrade', 'web2py upgrade': 'web2py upgrade',
'web2py upgraded; please restart it': 'web2py mis à jour; veuillez le redémarrer', 'web2py upgraded; please restart it': 'web2py mis à jour; veuillez le redémarrer',
'Working...': 'Working...', 'Working...': 'Travail en cours...',
'WSGI reference name': 'WSGI reference name', 'WSGI reference name': 'WSGI reference name',
'YES': 'OUI', '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 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 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', 'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out',
+2 -2
View File
@@ -352,9 +352,9 @@
'modules': 'modules', 'modules': 'modules',
'Multi User Mode': 'Multi User Mode', 'Multi User Mode': 'Multi User Mode',
'Must include at least %s %s': 'Moet ten minste bevatten %s %s', 'Must include at least %s %s': 'Moet ten minste bevatten %s %s',
'Must include at least %s lower case': 'Moet ten minste bevatten %s lower case', 'Must include at least %s lowercase': 'Moet ten minste bevatten %s kleine letter',
'Must include at least %s of the following : %s': 'Moet ten minste bevatten %s van het volgende : %s', 'Must include at least %s of the following : %s': 'Moet ten minste bevatten %s van het volgende : %s',
'Must include at least %s upper case': 'Moet ten minste bevatten %s upper case', 'Must include at least %s uppercase': 'Moet ten minste bevatten %s hoofdletter',
'new application "%s" created': 'nieuwe applicatie "%s" gemaakt', 'new application "%s" created': 'nieuwe applicatie "%s" gemaakt',
'new application "%s" imported': 'new application "%s" imported', 'new application "%s" imported': 'new application "%s" imported',
'New Application Wizard': 'Nieuwe Applicatie Wizard', 'New Application Wizard': 'Nieuwe Applicatie Wizard',
+2 -2
View File
@@ -356,9 +356,9 @@
'modules': 'модулі', 'modules': 'модулі',
'Multi User Mode': 'Multi User Mode', 'Multi User Mode': 'Multi User Mode',
'Must include at least %s %s': 'Має вміщувати щонайменше %s %s', 'Must include at least %s %s': 'Має вміщувати щонайменше %s %s',
'Must include at least %s lower case': 'Повинен включати щонайменше %s малих букв', 'Must include at least %s lowercase': 'Повинен включати щонайменше %s малих букв',
'Must include at least %s of the following : %s': 'Має включати не менше %s таких символів : %s', 'Must include at least %s of the following : %s': 'Має включати не менше %s таких символів : %s',
'Must include at least %s upper case': 'Повинен включати щонайменше %s великих букв', 'Must include at least %s uppercase': 'Повинен включати щонайменше %s великих букв',
'new application "%s" created': 'новий додаток "%s" створено', 'new application "%s" created': 'новий додаток "%s" створено',
'new application "%s" imported': 'new application "%s" imported', 'new application "%s" imported': 'new application "%s" imported',
'New Application Wizard': 'Майстер створення нового додатку', 'New Application Wizard': 'Майстер створення нового додатку',
+1 -1
View File
@@ -52,7 +52,7 @@ def verify_password(password):
if DEMO_MODE: if DEMO_MODE:
ret = True ret = True
elif not _config.get('password'): elif not _config.get('password'):
ret - False ret = False
elif _config['password'].startswith('pam_user:'): elif _config['password'].startswith('pam_user:'):
session.pam_user = _config['password'][9:].strip() session.pam_user = _config['password'][9:].strip()
import gluon.contrib.pam import gluon.contrib.pam
+3 -3
View File
@@ -314,8 +314,8 @@
// Vim does not support modifier only keys. // Vim does not support modifier only keys.
return false; return false;
} }
// TODO: Current bindings expect the character to be lower case, but // TODO: Current bindings expect the character to be lowercase, but
// it looks like vim key notation uses upper case. // it looks like vim key notation uses uppercase.
if (isUpperCase(lastPiece)) { if (isUpperCase(lastPiece)) {
pieces[pieces.length - 1] = lastPiece.toLowerCase(); pieces[pieces.length - 1] = lastPiece.toLowerCase();
} }
@@ -3644,7 +3644,7 @@
* Extract the regular expression from the query and return a Regexp object. * Extract the regular expression from the query and return a Regexp object.
* Returns null if the query is blank. * Returns null if the query is blank.
* If ignoreCase is passed in, the Regexp object will have the 'i' flag set. * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
* If smartCase is passed in, and the query contains upper case letters, * If smartCase is passed in, and the query contains uppercase letters,
* then ignoreCase is overridden, and the 'i' flag will not be set. * then ignoreCase is overridden, and the 'i' flag will not be set.
* If the query contains the /i in the flag part of the regular expression, * If the query contains the /i in the flag part of the regular expression,
* then both ignoreCase and smartCase are ignored, and 'i' will be passed * then both ignoreCase and smartCase are ignored, and 'i' will be passed
File diff suppressed because it is too large Load Diff
@@ -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
+43 -11
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow'); if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow'); else $('#' + id).show().fadeOut('slow');
}, },
ajax: function (u, s, t) { ajax: function (u, s, t, options) {
/*simple ajax function*/ /*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = ''; var query = '';
if (typeof s == 'string') { if (typeof s == 'string') {
var d = $(s).serialize(); var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&'); 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', type: 'POST',
url: u, url: u,
data: query, data: query,
success: function (msg) { success: success
if (t) { };
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg); //remove custom "done" option if exists
else t(msg); 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) { ajax_fields: function (target) {
/* /*
@@ -168,7 +198,8 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.w2p_flash', function () { doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this); var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow'); if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -316,6 +347,7 @@
'beforeSend': function (xhr, settings) { 'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target); xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
}, },
'success': function (data, status, xhr) { 'success': function (data, status, xhr) {
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div> <div class="clear"></div>
{{pass}} {{pass}}
{{if request.function=='graph_model':}} {{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2> <h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}} {{if not databases:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{=T("No databases in this application")}} {{=T("No databases in this application")}}
{{else:}} {{else:}}
<div class="btn-group"> <div id="vis"></div>
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> <link rel="stylesheet" href="{{=URL('admin','static','css/d3_graph.css')}}"/>
<i class="icon-download"></i> {{=T('Save model as...')}} <script>
<span class="caret"></span> // Define the d3 input data
</a> {{from gluon.serializers import json }}
<ul class="dropdown-menu"> var nodes = {{=XML(json(nodes))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li> var links = {{=XML(json(links))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li> d3_graph();
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li> </script>
<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'))}}
{{pass}} {{pass}}
{{pass}} {{pass}}
{{if request.function == 'manage':}} {{if request.function == 'manage':}}
<h2>{{=heading}}</h2> <h2>{{=heading}}</h2>
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}"> <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 --> <!-- 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'}} {{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):}} {{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')}} {{=button(URL('peek/%s/databases/sql.log'%app), 'sql.log')}}
{{pass}} {{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> </div>
<ul class="unstyled act_edit"> <ul class="unstyled act_edit">
{{for m in models:}} {{for m in models:}}
+1 -1
View File
@@ -46,7 +46,7 @@
</td> </td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> <a class="btn dropdown-toggle" data-toggle="dropdown">
{{=T('Manage')}} {{=T('Manage')}}
<span class="caret"></span> <span class="caret"></span>
</a> </a>
+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> <h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}} {{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}} {{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}} {{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}} {{=BEAUTIFY(response._vars)}}
{{pass}} {{pass}}
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}"> <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 --> <!-- 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'}} {{include 'web2py_ajax.html'}}
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment # ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0]))) ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict): if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses'] disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0) _cellborder=0, _cellspacing=0)
).xml() ).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(): def manage():
tables = manager_action['tables'] tables = manager_action['tables']
if isinstance(tables[0], str): if isinstance(tables[0], str):
@@ -699,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_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) ul_main.append(ul_t)
return ul_main 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
+43 -11
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow'); if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow'); else $('#' + id).show().fadeOut('slow');
}, },
ajax: function (u, s, t) { ajax: function (u, s, t, options) {
/*simple ajax function*/ /*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = ''; var query = '';
if (typeof s == 'string') { if (typeof s == 'string') {
var d = $(s).serialize(); var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&'); 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', type: 'POST',
url: u, url: u,
data: query, data: query,
success: function (msg) { success: success
if (t) { };
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg); //remove custom "done" option if exists
else t(msg); 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) { ajax_fields: function (target) {
/* /*
@@ -168,7 +198,8 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.w2p_flash', function () { doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this); var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow'); if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -316,6 +347,7 @@
'beforeSend': function (xhr, settings) { 'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target); xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
}, },
'success': function (data, status, xhr) { 'success': function (data, status, xhr) {
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div> <div class="clear"></div>
{{pass}} {{pass}}
{{if request.function=='graph_model':}} {{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2> <h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}} {{if not databases:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{=T("No databases in this application")}} {{=T("No databases in this application")}}
{{else:}} {{else:}}
<div class="btn-group"> <div id="vis"></div>
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> <link rel="stylesheet" href="{{=URL('admin','static','css/d3_graph.css')}}"/>
<i class="icon-download"></i> {{=T('Save model as...')}} <script>
<span class="caret"></span> // Define the d3 input data
</a> {{from gluon.serializers import json }}
<ul class="dropdown-menu"> var nodes = {{=XML(json(nodes))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li> var links = {{=XML(json(links))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li> d3_graph();
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li> </script>
<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'))}}
{{pass}} {{pass}}
{{pass}} {{pass}}
{{if request.function == 'manage':}} {{if request.function == 'manage':}}
<h2>{{=heading}}</h2> <h2>{{=heading}}</h2>
@@ -17,10 +17,10 @@
<tbody> <tbody>
<tr> <tr>
<td> <td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_win.zip">For Windows</a> <a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_win.zip">For Windows</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_win.zip">For Windows</a> <a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_win.zip">For Windows</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded red" href="http://github.com/web2py/web2py/">Git Repository</a> <a class="btn btn180 rounded red" href="http://github.com/web2py/web2py/">Git Repository</a>
@@ -28,19 +28,19 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_osx.zip">For Mac</a> <a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_osx.zip">For Mac</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_osx.zip">For Mac</a> <a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_osx.zip">For Mac</a>
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td> <td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_src.zip">Source Code</a> <a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_src.zip">Source Code</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_src.zip">Source Code</a> <a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_src.zip">Source Code</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded red" href="http://web2py.readthedocs.org/en/latest/">Source code docs</a> <a class="btn btn180 rounded red" href="http://web2py.readthedocs.org/en/latest/">Source code docs</a>
+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> <h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}} {{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}} {{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}} {{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}} {{=BEAUTIFY(response._vars)}}
{{pass}} {{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}}
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment # ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0]))) ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict): if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses'] disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0) _cellborder=0, _cellspacing=0)
).xml() ).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(): def manage():
tables = manager_action['tables'] tables = manager_action['tables']
if isinstance(tables[0], str): if isinstance(tables[0], str):
@@ -699,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_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) ul_main.append(ul_t)
return ul_main 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', '!langcode!': 'fr-ca',
'!langname!': 'Français (Canadien)', '!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', '"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} deleted': '%s lignes supprimées',
'%s %%{row} updated': '%s rangées mises à jour', '%s %%{row} updated': '%s lignes mises à jour',
'%s selected': '%s sélectionné', '%s selected': '%s sélectionné',
'%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d': '%Y-%m-%d',
'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '%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)}', '**%(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)', '**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)', '``**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',
'About': 'À propos', 'About': 'À propos',
'Access Control': "Contrôle d'accès", 'Access Control': "Contrôle d'accès",
'admin': 'admin', 'admin': 'admin',
'Administrative Interface': 'Administrative Interface', 'Administrative Interface': "Interface d'administration",
'Administrative interface': "Interface d'administration", 'Administrative interface': "Interface d'administration",
'Ajax Recipes': 'Recettes Ajax', 'Ajax Recipes': 'Recettes Ajax',
'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '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', 'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer', 'Check to delete': 'Cliquez pour supprimer',
'Check to delete:': 'Cliquez pour supprimer:', 'Check to delete:': 'Cliquez pour supprimer:',
'Clear CACHE?': 'Clear CACHE?', 'Clear CACHE?': 'Vider le CACHE?',
'Clear DISK': 'Clear DISK', 'Clear DISK': 'Vider le DISQUE',
'Clear RAM': 'Clear RAM', 'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client', 'Client IP': 'IP client',
'Community': 'Communauté', 'Community': 'Communauté',
'Components and Plugins': 'Components and Plugins', 'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini', 'Config.ini': 'Config.ini',
'Controller': 'Contrôleur', 'Controller': 'Contrôleur',
'Copyright': "Droit d'auteur", 'Copyright': "Droit d'auteur",
'Created By': 'Créé par',
'Created On': 'Créé le',
'Current request': 'Demande actuelle', 'Current request': 'Demande actuelle',
'Current response': 'Réponse actuelle', 'Current response': 'Réponse actuelle',
'Current session': 'Session en cours', 'Current session': 'Session en cours',
'customize me!': 'personnalisez-moi!', 'customize me!': 'personnalisez-moi!',
'data uploaded': 'données téléchargées', 'data uploaded': 'données téléchargées',
'Database': 'base de donné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)', 'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'db', 'db': 'db',
'DB Model': 'Modèle DB', 'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:', 'Delete:': 'Supprimer:',
'Demo': 'Démo', 'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement ', 'Deployment Recipes': 'Recettes de déploiement',
'Description': 'Descriptif', 'Description': 'Description',
'design': 'design', 'design': 'design',
'Design': 'Design', 'Design': 'Design',
'DISK': 'DISK', 'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys', '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.', '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', '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!', 'done!': 'fait!',
'Download': 'Téléchargement', 'Download': 'Téléchargement',
'E-mail': 'Courriel', 'E-mail': 'Courriel',
@@ -75,26 +78,27 @@
'Edit current record': "Modifier l'enregistrement courant", 'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil', 'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application', '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', 'enter an integer between %(min)g and %(max)g': 'entrer un entier compris entre %(min)g et %(max)g',
'Errors': 'Erreurs', 'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv', 'export as csv file': 'exporter sous forme de fichier csv',
'FAQ': 'faq', 'FAQ': 'FAQ',
'First name': 'Prénom', 'First name': 'Prénom',
'Forms and Validators': 'Formulaires et Validateurs', 'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites', 'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée', '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 %(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", 'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s",
'Groups': 'Groupes', 'Groups': 'Groupes',
'Hello World': 'Bonjour le monde', '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)})', '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', 'Home': 'Accueil',
'How did you get here?': 'How did you get here?', 'How did you get here?': 'How did you get here?',
'import': 'import', 'import': 'importer',
'Import/Export': 'Importer/Exporter', 'Import/Export': 'Importer/Exporter',
'Index': 'Index', 'Index': 'Index',
'insert new': 'insérer un nouveau', 'insert new': 'insérer un nouveau',
@@ -104,40 +108,47 @@
'Invalid email': 'Courriel invalide', 'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide', 'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide', 'invalid request': 'requête invalide',
'Key': 'Key', 'Is Active': 'Est actif',
'Key': 'Clé',
'Last name': 'Nom', 'Last name': 'Nom',
'Layout': 'Mise en page', 'Layout': 'Mise en page',
'Layout Plugins': 'Layout Plugins', 'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'layouts', 'Layouts': 'Mises en page',
'Live chat': 'Clavardage en direct', 'Live chat': 'Clavardage en direct',
'Live Chat': 'Live Chat', 'Live Chat': 'Clavardage en direct',
'Log In': 'Log In', 'Loading...': 'Chargement...',
'loading...': 'chargement...',
'Log In': 'Connexion',
'Logged in': 'Connecté', 'Logged in': 'Connecté',
'login': 'connectez-vous', 'login': 'connexion',
'Login': 'Connectez-vous', 'Login': 'Connexion',
'logout': 'déconnectez-vous', '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',
'Lost password?': 'Mot de passe perdu?',
'lost password?': 'mot de passe perdu?', 'lost password?': 'mot de passe perdu?',
'Main Menu': 'Menu principal', 'Main Menu': 'Menu principal',
'Manage %(action)s': 'Manage %(action)s', 'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control', 'Manage Access Control': 'Manage Access Control',
'Manage Cache': 'Manage Cache', 'Manage Cache': 'Gérer le Cache',
'Memberships': 'Memberships', 'Memberships': 'Memberships',
'Menu Model': 'Menu modèle', 'Menu Model': 'Menu modèle',
'My Sites': 'My Sites', 'Modified By': 'Modifié par',
'Modified On': 'Modifié le',
'My Sites': 'Mes sites',
'Name': 'Nom', 'Name': 'Nom',
'New Record': 'Nouvel enregistrement', 'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré', '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', 'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données", 'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**', 'Number of entries: **%s**': 'Number of entries: **%s**',
'Object or table name': 'Objet ou nom de table',
'Online book': 'Online book', 'Online book': 'Online book',
'Online examples': 'Exemples en ligne', 'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV", 'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine', 'Origin': 'Origine',
'Other Plugins': 'Other Plugins', 'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes', 'Other Recipes': 'Autres recettes',
'Overview': 'Présentation', 'Overview': 'Présentation',
'password': 'mot de passe', 'password': 'mot de passe',
@@ -145,33 +156,34 @@
"Password fields don't match": 'Les mots de passe ne correspondent pas', "Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission', 'Permission': 'Permission',
'Permissions': 'Permissions', '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', 'Plugins': 'Plugiciels',
'Powered by': 'Alimenté par', 'Powered by': 'Alimenté par',
'Preface': 'Préface', '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', 'previous 100 rows': '100 lignes précédentes',
'profile': 'profile', 'profile': 'profil',
'pygraphviz library not found': 'pygraphviz library not found', 'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python', 'Python': 'Python',
'Query:': 'Requête:', 'Query:': 'Requête:',
'Quick Examples': 'Examples Rapides', 'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM', 'RAM': 'RAM',
'RAM Cache Keys': 'RAM Cache Keys', '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.', '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', 'Readme': 'Lisez-moi',
'Recipes': 'Recettes', 'Recipes': 'Recettes',
'Record': 'enregistrement', 'Record': 'enregistrement',
'Record %(id)s created': 'Record %(id)s created', 'Record %(id)s created': 'Enregistrement %(id)s créé',
'Record %(id)s updated': 'Record %(id)s updated', 'Record %(id)s updated': 'Enregistrement %(id)s modifié',
'Record Created': 'Record Created', 'Record Created': 'Enregistrement créé',
'record does not exist': "l'archive n'existe pas", 'record does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement", 'Record ID': "ID de l'enregistrement",
'Record id': "id d'enregistrement", 'Record id': "id de l'enregistrement",
'Record Updated': 'Record Updated', 'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire", 'Register': "S'inscrire",
'register': "s'inscrire", 'register': "s'inscrire",
'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement", 'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie', 'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)', 'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
@@ -179,18 +191,18 @@
'Reset Password key': 'Réinitialiser le mot clé', 'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources', 'Resources': 'Ressources',
'Role': 'Rôle', 'Role': 'Rôle',
'Roles': 'Roles', 'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau', 'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées', 'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Save model as...', 'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique', 'Semantic': 'Sémantique',
'Services': 'Services', 'Services': 'Services',
'Sign Up': 'Sign Up', 'Sign Up': "S'inscrire",
'Size of cache:': 'Size of cache:', 'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état', 'state': 'état',
'Statistics': 'Statistics', 'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style', 'Stylesheet': 'Feuille de style',
'submit': 'submit', 'submit': 'soumettre',
'Submit': 'Soumettre', 'Submit': 'Soumettre',
'Support': 'Soutien', 'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', 'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
@@ -202,31 +214,31 @@
'The Views': 'Les Vues', 'The Views': 'Les Vues',
'This App': 'Cette Appli', 'This App': 'Cette Appli',
'This is a copy of the scaffolding application': "Ceci est une copie de l'application échafaudage", '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', 'Timestamp': 'Horodatage',
'Traceback': 'Traceback', 'Traceback': 'Traceback',
'Twitter': 'Twitter', 'Twitter': 'Twitter',
'unable to parse csv file': "incapable d'analyser le fichier cvs", 'unable to parse csv file': "incapable d'analyser le fichier cvs",
'Update:': 'Mise à jour:', '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.', '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': 'User', 'User': 'Utilisateur',
'User %(id)s Logged-in': 'Utilisateur %(id)s connecté', 'User %(id)s Logged-in': 'Utilisateur %(id)s connecté',
'User %(id)s Registered': 'Utilisateur %(id)s enregistré', 'User %(id)s Registered': 'Utilisateur %(id)s enregistré',
'User ID': 'ID utilisateur', 'User ID': 'ID utilisateur',
'User Voice': 'User Voice', 'User Voice': "Voix de l'utilisateur",
'Users': 'Users', 'Users': 'Utilisateurs',
'value already in database or empty': 'valeur déjà dans la base ou vide', 'value already in database or empty': 'valeur déjà dans la base ou inexistante',
'Verify Password': 'Vérifiez le mot de passe', 'Verify Password': 'Vérifiez le mot de passe',
'Videos': 'Vidéos', 'Videos': 'Vidéos',
'View': 'Présentation', 'View': 'Vue',
'Web2py': 'Web2py', 'Web2py': 'Web2py',
'Welcome': 'Bienvenu', 'Welcome': 'Bienvenue',
'Welcome %s': 'Bienvenue %s', 'Welcome %s': 'Bienvenue %s',
'Welcome to web2py': 'Bienvenue à web2py', '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', 'Which called the function %s located in the file %s': 'Qui a appelé la fonction %s se trouvant dans le fichier %s',
'Working...': 'Working...', 'Working...': 'Traitement en cours...',
'You are successfully running web2py': 'Vous roulez avec succès web2py', '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 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", '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)}', '**%(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)', '**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)', '``**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', 'About': 'À propos',
'Access Control': "Contrôle d'accès", 'Access Control': "Contrôle d'accès",
'admin': 'admin', 'admin': 'admin',
@@ -31,7 +33,7 @@
'Cache': 'Cache', 'Cache': 'Cache',
'Cache Cleared': 'Cache Cleared', '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 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', 'Cannot be empty': 'Ne peut pas être vide',
'change password': 'changer le mot de passe', 'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer', 'Check to delete': 'Cliquez pour supprimer',
@@ -41,10 +43,10 @@
'Clear RAM': 'Vider la RAM', 'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client', 'Client IP': 'IP client',
'Community': 'Communauté', 'Community': 'Communauté',
'Components and Plugins': 'Composants et Plugins', 'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini', 'Config.ini': 'Config.ini',
'Controller': 'Contrôleur', 'Controller': 'Contrôleur',
'Copyright': 'Copyright', 'Copyright': "Droit d'auteur",
'Created By': 'Créé par', 'Created By': 'Créé par',
'Created On': 'Créé le', 'Created On': 'Créé le',
'Current request': 'Demande actuelle', 'Current request': 'Demande actuelle',
@@ -55,8 +57,8 @@
'Database': 'base de données', 'Database': 'base de données',
'Database %s select': 'base de données %s selectionnée', 'Database %s select': 'base de données %s selectionnée',
'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'bdd', 'db': 'db',
'DB Model': 'Modèle BDD', 'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:', 'Delete:': 'Supprimer:',
'Demo': 'Démo', 'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement', 'Deployment Recipes': 'Recettes de déploiement',
@@ -71,12 +73,13 @@
"Don't know what to do?": 'Vous ne savez pas quoi faire?', "Don't know what to do?": 'Vous ne savez pas quoi faire?',
'done!': 'fait!', 'done!': 'fait!',
'Download': 'Téléchargement', 'Download': 'Téléchargement',
'E-mail': 'E-mail', 'E-mail': 'Courriel',
'Edit': 'Éditer', 'Edit': 'Éditer',
'Edit current record': "Modifier l'enregistrement courant", 'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil', 'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application', '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', 'enter an integer between %(min)g and %(max)g': 'entrez un entier entre %(min)g et %(max)g',
'Errors': 'Erreurs', 'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv', 'export as csv file': 'exporter sous forme de fichier csv',
@@ -85,22 +88,24 @@
'Forms and Validators': 'Formulaires et Validateurs', 'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites', 'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée', 'Function disabled': 'Fonction désactivée',
'Graph Model': 'Graph Model', 'Graph Model': 'Représentation graphique du modèle',
'Group ID': 'Groupe ID', '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', 'Groups': 'Groupes',
'Hello World': 'Bonjour le monde', 'Hello World': 'Bonjour le monde',
'Helping web2py': 'En train d’aider 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)})', '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', 'Home': 'Accueil',
'How did you get here?': 'Comment êtes-vous arrivé ici?', 'How did you get here?': 'How did you get here?',
'import': 'Importer', 'import': 'importer',
'Import/Export': 'Importer/Exporter', 'Import/Export': 'Importer/Exporter',
'Index': 'Index', 'Index': 'Index',
'insert new': 'insérer un nouveau', 'insert new': 'insérer un nouveau',
'insert new %s': 'insérer un nouveau %s', 'insert new %s': 'insérer un nouveau %s',
'Internal State': 'État interne', 'Internal State': 'État interne',
'Introduction': 'Introduction', 'Introduction': 'Présentation',
'Invalid email': 'E-mail invalide', 'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide', 'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide', 'invalid request': 'requête invalide',
'Is Active': 'Est actif', 'Is Active': 'Est actif',
@@ -109,12 +114,15 @@
'Layout': 'Mise en page', 'Layout': 'Mise en page',
'Layout Plugins': 'Plugins de mise en page', 'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'Mises en page', 'Layouts': 'Mises en page',
'Live chat': 'Chat en direct', 'Live chat': 'Clavardage en direct',
'Live Chat': 'Chat en direct', 'Live Chat': 'Clavardage en direct',
'Log In': 'Log In', 'Loading...': 'Chargement...',
'login': 'connectez-vous', 'loading...': 'chargement...',
'Login': 'Connectez-vous', 'Log In': 'Connexion',
'logout': 'déconnectez-vous', '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', 'Lost Password': 'Mot de passe perdu',
'Lost password?': 'Mot de passe perdu?', 'Lost password?': 'Mot de passe perdu?',
@@ -131,7 +139,7 @@
'Name': 'Nom', 'Name': 'Nom',
'New Record': 'Nouvel enregistrement', 'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré', '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', 'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données", 'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**', 'Number of entries: **%s**': 'Number of entries: **%s**',
@@ -140,55 +148,63 @@
'Online examples': 'Exemples en ligne', 'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV", 'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine', 'Origin': 'Origine',
'Other Plugins': 'Autres Plugins', 'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes', 'Other Recipes': 'Autres recettes',
'Overview': 'Présentation', 'Overview': 'Présentation',
'password': 'mot de passe',
'Password': 'Mot de passe', 'Password': 'Mot de passe',
"Password fields don't match": 'Les mots de passe ne correspondent pas', "Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission', 'Permission': 'Permission',
'Permissions': 'Permissions', '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', 'Powered by': 'Alimenté par',
'Preface': 'Préface', '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', 'previous 100 rows': '100 lignes précédentes',
'profile': 'profil',
'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable', 'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python', 'Python': 'Python',
'Query:': 'Requête:', 'Query:': 'Requête:',
'Quick Examples': 'Exemples Rapides', 'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM', 'RAM': 'RAM',
'RAM Cache Keys': 'Clés de cache de la RAM', 'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram vidée', '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.', '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', 'Readme': 'Lisez-moi',
'Recipes': 'Recettes', 'Recipes': 'Recettes',
'Record': 'enregistrement', '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 does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement", 'Record ID': "ID de l'enregistrement",
'Record id': "id d'enregistrement", 'Record id': "id de l'enregistrement",
'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire", 'Register': "S'inscrire",
'register': "s'inscrire", 'register': "s'inscrire",
'Registration identifier': "Identifiant d'enregistrement", 'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement", 'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)', 'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
'Request reset password': 'Demande de réinitialiser le mot clé', 'Request reset password': 'Demande de réinitialiser le mot clé',
'Reset Password key': 'Réinitialiser le mot clé', 'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources', 'Resources': 'Ressources',
'Role': 'Rôle', 'Role': 'Rôle',
'Roles': 'Roles', 'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau', 'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées', 'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Enregistrer le modèle sous...', 'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique', 'Semantic': 'Sémantique',
'Services': 'Services', 'Services': 'Services',
'Sign Up': 'Sign Up', 'Sign Up': "S'inscrire",
'Size of cache:': 'Taille du cache:', 'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état', 'state': 'état',
'Statistics': 'Statistiques', 'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style', 'Stylesheet': 'Feuille de style',
'submit': 'soumettre', 'submit': 'soumettre',
'Submit': 'Soumettre', 'Submit': 'Soumettre',
'Support': 'Support', 'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', 'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
'Table': 'tableau', 'Table': 'tableau',
'Table name': 'Nom du tableau', 'Table name': 'Nom du tableau',
+2 -2
View File
@@ -20,8 +20,8 @@
# YOU CAN COPY THIS FILE TO ANY APPLICATION'S ROOT DIRECTORY WITHOUT CHANGES! # YOU CAN COPY THIS FILE TO ANY APPLICATION'S ROOT DIRECTORY WITHOUT CHANGES!
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
from fileutils import abspath from gluon.fileutils import abspath
from languages import read_possible_languages from gluon.languages import read_possible_languages
possible_languages = read_possible_languages(abspath('applications', app)) possible_languages = read_possible_languages(abspath('applications', app))
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
File diff suppressed because one or more lines are too long
+71 -19
View File
@@ -12,6 +12,8 @@
$.error('web2py.js has already been loaded!'); $.error('web2py.js has already been loaded!');
} }
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
String.prototype.reverse = function () { String.prototype.reverse = function () {
return this.split('').reverse().join(''); return this.split('').reverse().join('');
}; };
@@ -38,8 +40,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow'); if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow'); else $('#' + id).show().fadeOut('slow');
}, },
ajax: function (u, s, t) { ajax: function (u, s, t, options) {
/*simple ajax function*/ /*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = ''; var query = '';
if (typeof s == 'string') { if (typeof s == 'string') {
var d = $(s).serialize(); var d = $(s).serialize();
@@ -59,18 +65,44 @@
query = pcs.join('&'); 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', type: 'POST',
url: u, url: u,
data: query, data: query,
success: function (msg) { success: success
if (t) { };
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg); //remove custom "done" option if exists
else t(msg); 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) { ajax_fields: function (target) {
/* /*
@@ -168,7 +200,8 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.w2p_flash', function () { doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this); var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow'); if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -289,7 +322,15 @@
form.submit(function (e) { form.submit(function (e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
web2py.ajax_page('post', url, form.serialize(), target, form);
var formData;
if (FORMDATA_IS_SUPPORTED) {
formData = new FormData(form[0]); // Allows file uploads.
} else {
formData = form.serialize(); // Fallback for older browsers.
}
web2py.ajax_page('post', url, formData, target, form);
e.preventDefault(); e.preventDefault();
}); });
form.on('click', web2py.formInputClickSelector, function (e) { form.on('click', web2py.formInputClickSelector, function (e) {
@@ -308,14 +349,22 @@
if (web2py.isUndefined(element)) element = $(document); if (web2py.isUndefined(element)) element = $(document);
/* if target is not there, fill it with something that there isn't in the page*/ /* if target is not there, fill it with something that there isn't in the page*/
if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; if (web2py.isUndefined(target) || target === '') target = 'w2p_none';
/* processData and contentType must be set to false when passing a FormData
object to jQuery.ajax. */
var isFormData = Object.prototype.toString.call(data) === '[object FormData]';
var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8';
if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */
$.ajax({ $.ajax({
'type': method, 'type': method,
'url': action, 'url': action,
'data': data, 'data': data,
'processData': !isFormData,
'contentType': contentType,
'beforeSend': function (xhr, settings) { 'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target); xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
}, },
'success': function (data, status, xhr) { 'success': function (data, status, xhr) {
@@ -667,8 +716,9 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Does not disable elements with 'data-w2p_disable' attribute
- Caches element value in 'w2p_enable_with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-w2p_disable_with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
disableFormElements: function (form) { disableFormElements: function (form) {
@@ -680,13 +730,15 @@
if (!web2py.isUndefined(disable)) { if (!web2py.isUndefined(disable)) {
return false; return false;
} }
if (web2py.isUndefined(disable_with)) { if (!element.is(':file')) { // Altering file input values is not allowed.
element.data('w2p_disable_with', element[method]()); if (web2py.isUndefined(disable_with)) {
element.data('w2p_disable_with', element[method]());
}
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
} }
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
}, },
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div> <div class="clear"></div>
{{pass}} {{pass}}
{{if request.function=='graph_model':}} {{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2> <h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}} {{if not databases:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{=T("No databases in this application")}} {{=T("No databases in this application")}}
{{else:}} {{else:}}
<div class="btn-group"> <div id="vis"></div>
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> <link rel="stylesheet" href="{{=URL('admin','static','css/d3_graph.css')}}"/>
<i class="icon-download"></i> {{=T('Save model as...')}} <script>
<span class="caret"></span> // Define the d3 input data
</a> {{from gluon.serializers import json }}
<ul class="dropdown-menu"> var nodes = {{=XML(json(nodes))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li> var links = {{=XML(json(links))}};
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li> d3_graph();
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li> </script>
<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'))}}
{{pass}} {{pass}}
{{pass}} {{pass}}
{{if request.function == 'manage':}} {{if request.function == 'manage':}}
<h2>{{=heading}}</h2> <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> <h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}} {{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}} {{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}} {{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}} {{=BEAUTIFY(response._vars)}}
{{pass}} {{pass}}
+2 -2
View File
@@ -7,7 +7,7 @@
# it is not safe to use as a generic.jsonp because of security implications. # it is not safe to use as a generic.jsonp because of security implications.
if response.view == 'generic.jsonp': if response.view == 'generic.jsonp':
raise HTTP(501,'generic.jsonp diasbled for security reasons') raise HTTP(501,'generic.jsonp disabled for security reasons')
try: try:
from gluon.serializers import json from gluon.serializers import json
@@ -20,4 +20,4 @@ except ImportError:
raise HTTP(405, 'JSON not available') raise HTTP(405, 'JSON not available')
except: except:
raise HTTP(405, 'JSON error') raise HTTP(405, 'JSON error')
}} }}
+1 -1
View File
@@ -27,4 +27,4 @@ Notice:
- no need to return a string - no need to return a string
even if the function is called via ajax. 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> </button>
{{=response.logo or ''}} {{=response.logo or ''}}
</div> </div>
<div class="collapse navbar-collapse navbar-ex1-collapse"> <div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}} {{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}}
</ul> </ul>
Vendored
+3 -2
View File
@@ -1,7 +1,8 @@
from fabric.api import * from fabric.api import *
from fabric.operations import put, get from fabric.operations import put, get
from fabric.contrib.files import exists from fabric.contrib.files import exists, append, uncomment
import os import os
import crypt
import datetime import datetime
import getpass import getpass
@@ -19,11 +20,11 @@ def create_user(username):
"""fab -H root@host create_user:username""" """fab -H root@host create_user:username"""
password = getpass.getpass('password for %s> ' % username) password = getpass.getpass('password for %s> ' % username)
run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username)) run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username))
local('ssh-copy-id %s' % env.hosts[0])
run('cp /etc/sudoers /tmp/sudoers.new') run('cp /etc/sudoers /tmp/sudoers.new')
append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True) append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True)
run('visudo -c -f /tmp/sudoers.new') run('visudo -c -f /tmp/sudoers.new')
run('EDITOR="cp /tmp/sudoers.new" visudo') run('EDITOR="cp /tmp/sudoers.new" visudo')
local('ssh-copy-id %s' % env.hosts[0])
uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes') uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes')
def install_web2py(): def install_web2py():
+2
View File
@@ -32,6 +32,7 @@ if PY2:
import cgi import cgi
import cookielib import cookielib
from xmlrpclib import ProtocolError from xmlrpclib import ProtocolError
from gluon.contrib import ipaddress
BytesIO = StringIO BytesIO = StringIO
reduce = reduce reduce = reduce
hashlib_md5 = hashlib.md5 hashlib_md5 = hashlib.md5
@@ -97,6 +98,7 @@ else:
from http import cookiejar as cookielib from http import cookiejar as cookielib
from xmlrpc.client import ProtocolError from xmlrpc.client import ProtocolError
import html # warning, this is the python3 module and not the web2py html module import html # warning, this is the python3 module and not the web2py html module
import ipaddress
hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8'))
iterkeys = lambda d: iter(d.keys()) iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values()) itervalues = lambda d: iter(d.values())
+20 -11
View File
@@ -54,7 +54,8 @@ def app_pack(app, request, raise_ex=False, filenames=None):
""" """
try: try:
if filenames is None: app_cleanup(app, request) if filenames is None:
app_cleanup(app, request)
filename = apath('../deposit/web2py.app.%s.w2p' % app, request) filename = apath('../deposit/web2py.app.%s.w2p' % app, request)
w2p_pack(filename, apath(app, request), filenames=filenames) w2p_pack(filename, apath(app, request), filenames=filenames)
return filename return filename
@@ -104,7 +105,8 @@ def app_cleanup(app, request):
if os.path.exists(path): if os.path.exists(path):
for f in os.listdir(path): for f in os.listdir(path):
try: try:
if f[:1] != '.': os.unlink(os.path.join(path, f)) if f[:1] != '.':
os.unlink(os.path.join(path, f))
except IOError: except IOError:
r = False r = False
@@ -113,7 +115,8 @@ def app_cleanup(app, request):
if os.path.exists(path): if os.path.exists(path):
for f in os.listdir(path): for f in os.listdir(path):
try: try:
if f[:1] != '.': recursive_unlink(os.path.join(path, f)) if f[:1] != '.':
recursive_unlink(os.path.join(path, f))
except (OSError, IOError): except (OSError, IOError):
r = False r = False
@@ -123,7 +126,8 @@ def app_cleanup(app, request):
CacheOnDisk(folder=path).clear() CacheOnDisk(folder=path).clear()
for f in os.listdir(path): for f in os.listdir(path):
try: try:
if f[:1] != '.': recursive_unlink(os.path.join(path, f)) if f[:1] != '.':
recursive_unlink(os.path.join(path, f))
except (OSError, IOError): except (OSError, IOError):
r = False r = False
return r return r
@@ -175,10 +179,9 @@ def app_create(app, request, force=False, key=None, info=False):
return False return False
try: try:
w2p_unpack('welcome.w2p', path) w2p_unpack('welcome.w2p', path)
for subfolder in [ for subfolder in ['models', 'views', 'controllers', 'databases',
'models', 'views', 'controllers', 'databases', 'modules', 'cron', 'errors', 'sessions', 'cache',
'modules', 'cron', 'errors', 'sessions', 'cache', 'languages', 'static', 'private', 'uploads']:
'languages', 'static', 'private', 'uploads']:
subpath = os.path.join(path, subfolder) subpath = os.path.join(path, subfolder)
if not os.path.exists(subpath): if not os.path.exists(subpath):
os.mkdir(subpath) os.mkdir(subpath)
@@ -368,7 +371,7 @@ def unzip(filename, dir, subfolder=''):
for name in sorted(zf.namelist()): for name in sorted(zf.namelist()):
if not name.startswith(subfolder): if not name.startswith(subfolder):
continue continue
#print name[n:] # print name[n:]
if name.endswith('/'): if name.endswith('/'):
folder = os.path.join(dir, name[n:]) folder = os.path.join(dir, name[n:])
if not os.path.exists(folder): if not os.path.exists(folder):
@@ -435,16 +438,22 @@ def add_path_first(path):
if not global_settings.web2py_runtime_gae: if not global_settings.web2py_runtime_gae:
site.addsitedir(path) site.addsitedir(path)
def try_mkdir(path): def try_mkdir(path):
if not os.path.exists(path): if not os.path.exists(path):
try: try:
os.mkdir(path) if os.path.islink(path):
# path is a broken link, try to mkdir the target of the link instead of the link itself.
os.mkdir(os.path.realpath(path))
else:
os.mkdir(path)
except OSError as e: except OSError as e:
if e.strerror == 'File exists': # In case of race condition. if e.strerror == 'File exists': # In case of race condition.
pass pass
else: else:
raise e raise e
def create_missing_folders(): def create_missing_folders():
if not global_settings.web2py_runtime_gae: if not global_settings.web2py_runtime_gae:
for path in ('applications', 'deposit', 'site-packages', 'logs'): for path in ('applications', 'deposit', 'site-packages', 'logs'):
+1038
View File
File diff suppressed because it is too large Load Diff
+44 -31
View File
@@ -41,11 +41,12 @@ import imp
import logging import logging
import types import types
from functools import reduce from functools import reduce
logger = logging.getLogger("web2py")
from gluon import rewrite from gluon import rewrite
from gluon.custom_import import custom_import_install from gluon.custom_import import custom_import_install
import py_compile import py_compile
logger = logging.getLogger("web2py")
is_pypy = settings.global_settings.is_pypy is_pypy = settings.global_settings.is_pypy
is_gae = settings.global_settings.web2py_runtime_gae is_gae = settings.global_settings.web2py_runtime_gae
is_jython = settings.global_settings.is_jython is_jython = settings.global_settings.is_jython
@@ -111,7 +112,7 @@ class mybuiltin(object):
NOTE could simple use a dict and populate it, NOTE could simple use a dict and populate it,
NOTE not sure if this changes things though if monkey patching import..... NOTE not sure if this changes things though if monkey patching import.....
""" """
#__builtins__ # __builtins__
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return getattr(builtin, key) return getattr(builtin, key)
@@ -185,7 +186,7 @@ def LOAD(c=None, f='index', args=None, vars=None,
else: else:
statement = "$.web2py.component('%s','%s');" % (url, target) statement = "$.web2py.component('%s','%s');" % (url, target)
attr['_data-w2p_remote'] = url attr['_data-w2p_remote'] = url
if not target is None: if target is not None:
return DIV(content, **attr) return DIV(content, **attr)
else: else:
@@ -211,7 +212,8 @@ def LOAD(c=None, f='index', args=None, vars=None,
request.env.path_info request.env.path_info
other_request.cid = target other_request.cid = target
other_request.env.http_web2py_component_element = target other_request.env.http_web2py_component_element = target
other_request.restful = types.MethodType(request.restful.__func__, other_request) # A bit nasty but needed to use LOAD on action decorates with @request.restful() other_request.restful = types.MethodType(request.restful.__func__, other_request)
# A bit nasty but needed to use LOAD on action decorates with @request.restful()
other_response.view = '%s/%s.%s' % (c, f, other_request.extension) other_response.view = '%s/%s.%s' % (c, f, other_request.extension)
other_environment = copy.copy(current.globalenv) # NASTY other_environment = copy.copy(current.globalenv) # NASTY
@@ -405,7 +407,7 @@ def build_environment(request, response, session, store_current=True):
""" """
Build the environment dictionary into which web2py files are executed. Build the environment dictionary into which web2py files are executed.
""" """
#h,v = html,validators # h,v = html,validators
environment = dict(_base_environment_) environment = dict(_base_environment_)
if not request.env: if not request.env:
@@ -418,7 +420,7 @@ def build_environment(request, response, session, store_current=True):
r'^%s/%s/\w+\.py$' % (request.controller, request.function) r'^%s/%s/\w+\.py$' % (request.controller, request.function)
] ]
t = environment['T'] = translator(os.path.join(request.folder,'languages'), t = environment['T'] = translator(os.path.join(request.folder, 'languages'),
request.env.http_accept_language) request.env.http_accept_language)
c = environment['cache'] = Cache(request) c = environment['cache'] = Cache(request)
@@ -506,10 +508,12 @@ def compile_models(folder):
save_pyc(filename) save_pyc(filename)
os.unlink(filename) os.unlink(filename)
def find_exposed_functions(data): def find_exposed_functions(data):
data = regex_longcomments.sub('',data) data = regex_longcomments.sub('', data)
return regex_expose.findall(data) return regex_expose.findall(data)
def compile_controllers(folder): def compile_controllers(folder):
""" """
Compiles all the controllers in the application specified by `folder` Compiles all the controllers in the application specified by `folder`
@@ -524,16 +528,19 @@ def compile_controllers(folder):
command = data + "\nresponse._vars=response._caller(%s)\n" % \ command = data + "\nresponse._vars=response._caller(%s)\n" % \
function function
filename = pjoin(folder, 'compiled', filename = pjoin(folder, 'compiled',
'controllers.%s.%s.py' % (fname[:-3],function)) 'controllers.%s.%s.py' % (fname[:-3], function))
write_file(filename, command) write_file(filename, command)
save_pyc(filename) save_pyc(filename)
os.unlink(filename) os.unlink(filename)
def model_cmp(a, b, sep='.'): def model_cmp(a, b, sep='.'):
return cmp(a.count(sep), b.count(sep)) or cmp(a, b) return cmp(a.count(sep), b.count(sep)) or cmp(a, b)
def model_cmp_sep(a, b, sep=os.path.sep): def model_cmp_sep(a, b, sep=os.path.sep):
return model_cmp(a,b,sep) return model_cmp(a, b, sep)
def run_models_in(environment): def run_models_in(environment):
""" """
@@ -544,7 +551,7 @@ def run_models_in(environment):
request = current.request request = current.request
folder = request.folder folder = request.folder
c = request.controller c = request.controller
#f = environment['request'].function # f = environment['request'].function
response = current.response response = current.response
path = pjoin(folder, 'models') path = pjoin(folder, 'models')
@@ -557,9 +564,11 @@ def run_models_in(environment):
models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), model_cmp_sep) models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), model_cmp_sep)
else: else:
if compiled: if compiled:
models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0), key=lambda f: '{0:03d}'.format(f.count('.')) + f) models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0),
key=lambda f: '{0:03d}'.format(f.count('.')) + f)
else: else:
models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), key=lambda f: '{0:03d}'.format(f.count(os.path.sep)) + f) models = sorted(listdir(path, '^\w+\.py$', 0, sort=False),
key=lambda f: '{0:03d}'.format(f.count(os.path.sep)) + f)
models_to_run = None models_to_run = None
for model in models: for model in models:
@@ -570,10 +579,10 @@ def run_models_in(environment):
if models_to_run: if models_to_run:
if compiled: if compiled:
n = len(cpath)+8 n = len(cpath)+8
fname = model[n:-4].replace('.','/')+'.py' fname = model[n:-4].replace('.', '/')+'.py'
else: else:
n = len(path)+1 n = len(path)+1
fname = model[n:].replace(os.path.sep,'/') fname = model[n:].replace(os.path.sep, '/')
if not regex.search(fname) and c != 'appadmin': if not regex.search(fname) and c != 'appadmin':
continue continue
elif compiled: elif compiled:
@@ -583,6 +592,7 @@ def run_models_in(environment):
ccode = getcfs(model, model, f) ccode = getcfs(model, model, f)
restricted(ccode, environment, layer=model) restricted(ccode, environment, layer=model)
def run_controller_in(controller, function, environment): def run_controller_in(controller, function, environment):
""" """
Runs the controller.function() (for the app specified by Runs the controller.function() (for the app specified by
@@ -596,13 +606,13 @@ def run_controller_in(controller, function, environment):
badc = 'invalid controller (%s/%s)' % (controller, function) badc = 'invalid controller (%s/%s)' % (controller, function)
badf = 'invalid function (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function)
if os.path.exists(cpath): if os.path.exists(cpath):
filename = pjoin(cpath, 'controllers.%s.%s.pyc' filename = pjoin(cpath, 'controllers.%s.%s.pyc' % (controller, function))
% (controller, function)) try:
if not os.path.exists(filename): ccode = getcfs(filename, filename, lambda: read_pyc(filename))
except IOError:
raise HTTP(404, raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badf, rewrite.THREAD_LOCAL.routes.error_message % badf,
web2py_error=badf) web2py_error=badf)
ccode = getcfs(filename, filename, lambda: read_pyc(filename))
elif function == '_TEST': elif function == '_TEST':
# TESTING: adjust the path to include site packages # TESTING: adjust the path to include site packages
from gluon.settings import global_settings from gluon.settings import global_settings
@@ -623,24 +633,24 @@ def run_controller_in(controller, function, environment):
code += TEST_CODE code += TEST_CODE
ccode = compile2(code, filename) ccode = compile2(code, filename)
else: else:
filename = pjoin(folder, 'controllers/%s.py' filename = pjoin(folder, 'controllers/%s.py' % controller)
% controller) try:
if not os.path.exists(filename): code = getcfs(filename, filename, lambda: read_file(filename))
except IOError:
raise HTTP(404, raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badc, rewrite.THREAD_LOCAL.routes.error_message % badc,
web2py_error=badc) web2py_error=badc)
code = getcfs(filename, filename, lambda: read_file(filename))
exposed = find_exposed_functions(code) exposed = find_exposed_functions(code)
if not function in exposed: if function not in exposed:
raise HTTP(404, raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badf, rewrite.THREAD_LOCAL.routes.error_message % badf,
web2py_error=badf) web2py_error=badf)
code = "%s\nresponse._vars=response._caller(%s)" % (code, function) code = "%s\nresponse._vars=response._caller(%s)" % (code, function)
layer = "%s:%s" % (filename, 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) restricted(ccode, environment, layer=filename)
response = current.response response = environment["response"]
vars = response._vars vars = response._vars
if response.postprocessing: if response.postprocessing:
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
@@ -678,7 +688,7 @@ def run_view_in(environment):
layer = 'file stream' layer = 'file stream'
else: else:
filename = pjoin(folder, 'views', view) filename = pjoin(folder, 'views', view)
if os.path.exists(cpath): # compiled views if os.path.exists(cpath): # compiled views
x = view.replace('/', '.') x = view.replace('/', '.')
files = ['views.%s.pyc' % x] files = ['views.%s.pyc' % x]
is_compiled = os.path.exists(pjoin(cpath, files[0])) is_compiled = os.path.exists(pjoin(cpath, files[0]))
@@ -705,16 +715,19 @@ def run_view_in(environment):
raise HTTP(404, raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badv, rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv) web2py_error=badv)
layer = filename
# Compile the template
ccode = parse_template(view,
pjoin(folder, 'views'),
context=environment)
# if the view is not compiled
if not layer:
# Compile the template
ccode = parse_template(view,
pjoin(folder, 'views'),
context=environment)
layer = filename
restricted(ccode, environment, layer=layer) restricted(ccode, environment, layer=layer)
# parse_template saves everything in response body # parse_template saves everything in response body
return environment['response'].body.getvalue() return environment['response'].body.getvalue()
def remove_compiled_application(folder): def remove_compiled_application(folder):
""" """
Deletes the folder `compiled` containing the compiled application. Deletes the folder `compiled` containing the compiled application.
+3 -3
View File
@@ -330,7 +330,7 @@ CONTENT_TYPE = {
'.lha': 'application/x-lha', '.lha': 'application/x-lha',
'.lhs': 'text/x-literate-haskell', '.lhs': 'text/x-literate-haskell',
'.lhz': 'application/x-lhz', '.lhz': 'application/x-lhz',
'.load' : 'text/html', '.load': 'text/html',
'.log': 'text/x-log', '.log': 'text/x-log',
'.lrz': 'application/x-lrzip', '.lrz': 'application/x-lrzip',
'.ltx': 'text/x-tex', '.ltx': 'text/x-tex',
@@ -823,7 +823,7 @@ CONTENT_TYPE = {
'.xsd': 'application/xml', '.xsd': 'application/xml',
'.xsl': 'application/xslt+xml', '.xsl': 'application/xslt+xml',
'.xslfo': 'text/x-xslfo', '.xslfo': 'text/x-xslfo',
'.xslm' : 'application/vnd.ms-excel.sheet.macroEnabled.12', '.xslm': 'application/vnd.ms-excel.sheet.macroEnabled.12',
'.xslt': 'application/xslt+xml', '.xslt': 'application/xslt+xml',
'.xspf': 'application/xspf+xml', '.xspf': 'application/xspf+xml',
'.xul': 'application/vnd.mozilla.xul+xml', '.xul': 'application/vnd.mozilla.xul+xml',
@@ -843,7 +843,7 @@ def contenttype(filename, default='text/plain'):
""" """
Returns the Content-Type string matching extension of the given filename. Returns the Content-Type string matching extension of the given filename.
""" """
filename=to_native(filename) filename = to_native(filename)
i = filename.rfind('.') i = filename.rfind('.')
if i >= 0: if i >= 0:
default = CONTENT_TYPE.get(filename[i:].lower(), default) default = CONTENT_TYPE.get(filename[i:].lower(), default)
+4 -6
View File
@@ -7,15 +7,13 @@ db = get_db()
""" """
import os import os
from gluon import * from gluon import *
from pydal.adapters import ADAPTERS, PostgreSQLAdapter from pydal.adapters import adapters, PostgrePsyco
from pydal.helpers.classes import UseDatabaseStoredFile from pydal.helpers.classes import DatabaseStoredFile
class HerokuPostgresAdapter(UseDatabaseStoredFile,PostgreSQLAdapter): @adapters.register_for('postgres')
drivers = ('psycopg2',) class HerokuPostgresAdapter(DatabaseStoredFile, PostgrePsyco):
uploads_in_blob = True uploads_in_blob = True
ADAPTERS['postgres'] = HerokuPostgresAdapter
def get_db(name = None, pool_size=10): def get_db(name = None, pool_size=10):
if not name: if not name:
names = [n for n in os.environ.keys() names = [n for n in os.environ.keys()
+1 -1
View File
@@ -45,7 +45,7 @@ class RESIZE(object):
background = Image.new('RGBA', (self.nx, self.ny), (255, 255, 255, 0)) background = Image.new('RGBA', (self.nx, self.ny), (255, 255, 255, 0))
background.paste( background.paste(
img, img,
((self.nx - img.size[0]) / 2, (self.ny - img.size[1]) / 2)) ((self.nx - img.size[0]) // 2, (self.ny - img.size[1]) // 2))
background.save(s, 'JPEG', quality=self.quality) background.save(s, 'JPEG', quality=self.quality)
else: else:
img.save(s, 'JPEG', quality=self.quality) img.save(s, 'JPEG', quality=self.quality)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -506,9 +506,9 @@ def ldap_auth(server='ldap',
l = [] l = []
for group in ldap_groups_of_the_user: for group in ldap_groups_of_the_user:
if group in group_mapping: if group in group_mapping:
l += group_mapping[group] l.append(group_mapping[group])
ldap_groups_of_the_user = l 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 # 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: except AttributeError as e:
db_user_id = db.auth_user.insert(email=username, first_name=username) db_user_id = db.auth_user.insert(email=username, first_name=username)
if not db_user_id: if not db_user_id:
logging.error( logger.error(
'There is no username or email for %s!' % username) 'There is no username or email for %s!' % username)
raise raise
# if old pydal version, assume this is a relational database which can do joins # 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): 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_group_id[group.role] = group.id
db_groups_of_the_user.append(group.role) 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 auth_membership_changed = False
# #
+57 -26
View File
@@ -5,12 +5,19 @@
# license MIT/BSD/GPL # license MIT/BSD/GPL
from __future__ import print_function from __future__ import print_function
import re import re
import sys
import urllib import urllib
from gluon._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange
from gluon.utils import local_html_escape as escape
from ast import parse as ast_parse
import ast import ast
PY2 = sys.version_info[0] == 2
if PY2:
from urllib import quote as urllib_quote
from string import maketrans
else:
from urllib.parse import quote as urllib_quote
maketrans = str.maketrans
""" """
TODO: next version should use MathJax TODO: next version should use MathJax
@@ -544,7 +551,7 @@ regex_code = re.compile(
'(' + META + '|' + DISABLED_META + r'|````)|(``(?P<t>.+?)``(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[^\]]*)\])?)?)', '(' + META + '|' + DISABLED_META + r'|````)|(``(?P<t>.+?)``(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[^\]]*)\])?)?)',
re.S) re.S)
regex_strong = re.compile(r'\*\*(?P<t>[^\s*]+( +[^\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_em = re.compile(r"''(?P<t>([^\s']| |'(?!'))+)''")
regex_num = re.compile(r"^\s*[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\s*$") regex_num = re.compile(r"^\s*[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\s*$")
regex_list = re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$') regex_list = re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$')
@@ -564,6 +571,29 @@ ttab_in = maketrans("'`:*~\\[]{}@$+-.#\n", '\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14
ttab_out = maketrans('\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x05', "'`:*~\\[]{}@$+-.#\n") ttab_out = maketrans('\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x05', "'`:*~\\[]{}@$+-.#\n")
regex_quote = re.compile('(?P<name>\w+?)\s*\=\s*') regex_quote = re.compile('(?P<name>\w+?)\s*\=\s*')
def local_html_escape(data, quote=False):
"""
Works with bytes.
Replace special characters "&", "<" and ">" to HTML-safe sequences.
If the optional flag quote is true (the default), the quotation mark
characters, both double quote (") and single quote (') characters are also
translated.
"""
if PY2:
import cgi
data = cgi.escape(data, quote)
return data.replace("'", "&#x27;") if quote else data
else:
import html
if isinstance(data, str):
return html.escape(data, quote=quote)
data = data.replace(b"&", b"&amp;") # Must be done first!
data = data.replace(b"<", b"&lt;")
data = data.replace(b">", b"&gt;")
if quote:
data = data.replace(b'"', b"&quot;")
data = data.replace(b'\'', b"&#x27;")
return data
def make_dict(b): def make_dict(b):
return '{%s}' % regex_quote.sub("'\g<name>':", b) return '{%s}' % regex_quote.sub("'\g<name>':", b)
@@ -579,7 +609,7 @@ def safe_eval(node_or_string, env):
_safe_names = {'None': None, 'True': True, 'False': False} _safe_names = {'None': None, 'True': True, 'False': False}
_safe_names.update(env) _safe_names.update(env)
if isinstance(node_or_string, basestring): if isinstance(node_or_string, basestring):
node_or_string = ast_parse(node_or_string, mode='eval') node_or_string = ast.parse(node_or_string, mode='eval')
if isinstance(node_or_string, ast.Expression): if isinstance(node_or_string, ast.Expression):
node_or_string = node_or_string.body node_or_string = node_or_string.body
@@ -599,14 +629,14 @@ def safe_eval(node_or_string, env):
if node.id in _safe_names: if node.id in _safe_names:
return _safe_names[node.id] return _safe_names[node.id]
elif isinstance(node, ast.BinOp) and \ elif isinstance(node, ast.BinOp) and \
isinstance(node.op, (Add, Sub)) and \ isinstance(node.op, (ast.Add, ast.Sub)) and \
isinstance(node.right, Num) and \ isinstance(node.right, ast.Num) and \
isinstance(node.right.n, complex) and \ isinstance(node.right.n, complex) and \
isinstance(node.left, Num) and \ isinstance(node.left, ast.Num) and \
isinstance(node.left.n, (int, long, float)): isinstance(node.left.n, (int, long, float)):
left = node.left.n left = node.left.n
right = node.right.n right = node.right.n
if isinstance(node.op, Add): if isinstance(node.op, ast.Add):
return left + right return left + right
else: else:
return left - right return left - right
@@ -765,7 +795,7 @@ def render(text,
'<table><tbody><tr class="first"><td>a</td><td>b</td></tr><tr class="even"><td>c</td><td>d</td></tr></tbody></table>' '<table><tbody><tr class="first"><td>a</td><td>b</td></tr><tr class="even"><td>c</td><td>d</td></tr></tbody></table>'
>>> render("----\\nhello world\\n----\\n") >>> render("----\\nhello world\\n----\\n")
'<blockquote>hello world</blockquote>' '<blockquote><p>hello world</p></blockquote>'
>>> render('[[myanchor]]') >>> render('[[myanchor]]')
'<p><span class="anchor" id="markmin_myanchor"></span></p>' '<p><span class="anchor" id="markmin_myanchor"></span></p>'
@@ -946,7 +976,8 @@ def render(text,
if protolinks == "default": if protolinks == "default":
protolinks = protolinks_simple protolinks = protolinks_simple
pp = '\n' if pretty_print else '' pp = '\n' if pretty_print else ''
text = to_native(text) text = text if text is None or isinstance(text, str) else text.decode('utf8', 'strict')
if not (isinstance(text, str)): if not (isinstance(text, str)):
text = str(text or '') text = str(text or '')
text = regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), text) text = regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), text)
@@ -994,7 +1025,7 @@ def render(text,
return LINK return LINK
text = regex_link.sub(mark_link, text) text = regex_link.sub(mark_link, text)
text = escape(text) text = local_html_escape(text)
if protolinks: if protolinks:
text = regex_proto.sub(lambda m: protolinks(*m.group('p', 'k')), text) text = regex_proto.sub(lambda m: protolinks(*m.group('p', 'k')), text)
@@ -1035,7 +1066,7 @@ def render(text,
if pend and mtag == '.': # paragraph in a list: if pend and mtag == '.': # paragraph in a list:
out.append(etags.pop()) out.append(etags.pop())
ltags.pop() ltags.pop()
for i in xrange(lent - lev): for i in range(lent - lev):
out.append('<' + tag + '>' + pp) out.append('<' + tag + '>' + pp)
etags.append('</' + tag + '>' + pp) etags.append('</' + tag + '>' + pp)
lev += 1 lev += 1
@@ -1044,7 +1075,7 @@ def render(text,
elif lent == lev: elif lent == lev:
if tlev[-1] != tag: if tlev[-1] != tag:
# type of list is changed (ul<=>ol): # type of list is changed (ul<=>ol):
for i in xrange(ltags.count(lent)): for i in range(ltags.count(lent)):
ltags.pop() ltags.pop()
out.append(etags.pop()) out.append(etags.pop())
tlev[-1] = tag tlev[-1] = tag
@@ -1209,7 +1240,7 @@ def render(text,
s = '<blockquote%s%s>%s</blockquote>%s' \ s = '<blockquote%s%s>%s</blockquote>%s' \
% (t_cls, % (t_cls,
t_id, t_id,
'\n'.join(strings[bq_begin:lineno]), pp) render('\n'.join(strings[bq_begin:lineno])), pp)
mtag = 'q' mtag = 'q'
else: else:
s = '<hr />' s = '<hr />'
@@ -1322,10 +1353,10 @@ def render(text,
t, a, k, p, w = m.group('t', 'a', 'k', 'p', 'w') t, a, k, p, w = m.group('t', 'a', 'k', 'p', 'w')
if not k: if not k:
return m.group(0) return m.group(0)
k = escape(k) k = local_html_escape(k)
t = t or '' t = t or ''
style = 'width:%s' % w if w else '' style = 'width:%s' % w if w else ''
title = ' title="%s"' % escape(a).replace(META, DISABLED_META) if a else '' title = ' title="%s"' % local_html_escape(a).replace(META, DISABLED_META) if a else ''
p_begin = p_end = '' p_begin = p_end = ''
if p == 'center': if p == 'center':
p_begin = '<p style="text-align:center">' p_begin = '<p style="text-align:center">'
@@ -1349,7 +1380,7 @@ def render(text,
autolinks, protolinks, class_prefix, id_prefix, pretty_print) autolinks, protolinks, class_prefix, id_prefix, pretty_print)
return '<%(p)s controls="controls"%(title)s%(style)s><source src="%(k)s" />%(t)s</%(p)s>' \ return '<%(p)s controls="controls"%(title)s%(style)s><source src="%(k)s" />%(t)s</%(p)s>' \
% dict(p=p, title=title, style=style, k=k, t=t) % dict(p=p, title=title, style=style, k=k, t=t)
alt = ' alt="%s"' % escape(t).replace(META, DISABLED_META) if t else '' alt = ' alt="%s"' % local_html_escape(t).replace(META, DISABLED_META) if t else ''
return '%(begin)s<img src="%(k)s"%(alt)s%(title)s%(style)s />%(end)s' \ return '%(begin)s<img src="%(k)s"%(alt)s%(title)s%(style)s />%(end)s' \
% dict(begin=p_begin, k=k, alt=alt, title=title, style=style, end=p_end) % dict(begin=p_begin, k=k, alt=alt, title=title, style=style, end=p_end)
@@ -1358,12 +1389,12 @@ def render(text,
if not k and not t: if not k and not t:
return m.group(0) return m.group(0)
t = t or '' t = t or ''
a = escape(a) if a else '' a = local_html_escape(a) if a else ''
if k: if k:
if '#' in k and ':' not in k.split('#')[0]: if '#' in k and ':' not in k.split('#')[0]:
# wikipage, not external url # wikipage, not external url
k = k.replace('#', '#' + id_prefix) k = k.replace('#', '#' + id_prefix)
k = escape(k) k = local_html_escape(k)
title = ' title="%s"' % a.replace(META, DISABLED_META) if a else '' title = ' title="%s"' % a.replace(META, DISABLED_META) if a else ''
target = ' target="_blank"' if p == 'popup' else '' target = ' target="_blank"' if p == 'popup' else ''
t = render(t, {}, {}, 'br', URL, environment, latex, None, t = render(t, {}, {}, 'br', URL, environment, latex, None,
@@ -1373,7 +1404,7 @@ def render(text,
if t == 'NEWLINE' and not a: if t == 'NEWLINE' and not a:
return '<br />' + pp return '<br />' + pp
return '<span class="anchor" id="%s">%s</span>' % ( return '<span class="anchor" id="%s">%s</span>' % (
escape(id_prefix + t), local_html_escape(id_prefix + t),
render(a, {}, {}, 'br', URL, render(a, {}, {}, 'br', URL,
environment, latex, autolinks, environment, latex, autolinks,
protolinks, class_prefix, protolinks, class_prefix,
@@ -1399,7 +1430,7 @@ def render(text,
def expand_meta(m): def expand_meta(m):
code, b, p, s = segments.pop(0) code, b, p, s = segments.pop(0)
if code is None or m.group() == DISABLED_META: if code is None or m.group() == DISABLED_META:
return escape(s) return local_html_escape(s)
if b in extra: if b in extra:
if code[:1] == '\n': if code[:1] == '\n':
code = code[1:] code = code[1:]
@@ -1411,7 +1442,7 @@ def render(text,
return str(extra[b](code)) return str(extra[b](code))
elif b == 'cite': elif b == 'cite':
return '[' + ','.join('<a href="#%s" class="%s">%s</a>' % return '[' + ','.join('<a href="#%s" class="%s">%s</a>' %
(id_prefix + d, b, d) for d in escape(code).split(',')) + ']' (id_prefix + d, b, d) for d in local_html_escape(code).split(',')) + ']'
elif b == 'latex': elif b == 'latex':
return LATEX % urllib_quote(code) return LATEX % urllib_quote(code)
elif b in html_colors: elif b in html_colors:
@@ -1426,12 +1457,12 @@ def render(text,
% (fg, bg, render(code, {}, {}, 'br', URL, environment, latex, % (fg, bg, render(code, {}, {}, 'br', URL, environment, latex,
autolinks, protolinks, class_prefix, id_prefix, pretty_print)) autolinks, protolinks, class_prefix, id_prefix, pretty_print))
cls = ' class="%s%s"' % (class_prefix, b) if b and b != 'id' else '' cls = ' class="%s%s"' % (class_prefix, b) if b and b != 'id' else ''
id = ' id="%s%s"' % (id_prefix, escape(p)) if p else '' id = ' id="%s%s"' % (id_prefix, local_html_escape(p)) if p else ''
beg = (code[:1] == '\n') beg = (code[:1] == '\n')
end = [None, -1][code[-1:] == '\n'] end = [None, -1][code[-1:] == '\n']
if beg and end: if beg and end:
return '<pre><code%s%s>%s</code></pre>%s' % (cls, id, escape(code[1:-1]), pp) return '<pre><code%s%s>%s</code></pre>%s' % (cls, id, local_html_escape(code[1:-1]), pp)
return '<code%s%s>%s</code>' % (cls, id, escape(code[beg:end])) return '<code%s%s>%s</code>' % (cls, id, local_html_escape(code[beg:end]))
text = regex_expand_meta.sub(expand_meta, text) text = regex_expand_meta.sub(expand_meta, text)
+3
View File
@@ -21,6 +21,9 @@ def MemcacheClient(*a, **b):
class MemcacheClientObj(Client): class MemcacheClientObj(Client):
def initialize(self):
pass
meta_storage = {} meta_storage = {}
max_time_expire = 24*3600 max_time_expire = 24*3600
+4 -1
View File
@@ -10,8 +10,11 @@ Original author: Zachary Voase
Modified for inclusion into web2py by: Ross Peoples <ross.peoples@gmail.com> Modified for inclusion into web2py by: Ross Peoples <ross.peoples@gmail.com>
""" """
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from StringIO import StringIO # The pure-Python StringIO supports unicode.
import re import re
+18 -8
View File
@@ -8,30 +8,40 @@ Created by: Ross Peoples <ross.peoples@gmail.com>
Modified by: Massimo Di Pierro <massimo.dipierro@gmail.com> Modified by: Massimo Di Pierro <massimo.dipierro@gmail.com>
""" """
import cssmin from . import cssmin
import jsmin from . import jsmin
import os import os
import hashlib import hashlib
import re import re
import sys
PY2 = sys.version_info[0] == 2
if PY2:
hashlib_md5 = hashlib.md5
else:
hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8'))
def open_py23(filename, mode):
if PY2:
f = open(filename, mode + 'b')
else:
f = open(filename, mode, encoding="utf8")
return f
def read_binary_file(filename): def read_binary_file(filename):
f = open(filename, 'rb') f = open_py23(filename, 'r')
data = f.read() data = f.read()
f.close() f.close()
return data return data
def write_binary_file(filename, data): def write_binary_file(filename, data):
f = open(filename, 'wb') f = open_py23(filename, 'w')
f.write(data) f.write(data)
f.close() f.close()
def fix_links(css, static_path): def fix_links(css, static_path):
return re.sub(r'url\((["\'])\.\./', 'url(\\1' + static_path, css) return re.sub(r'url\((["\'])\.\./', 'url(\\1' + static_path, css)
def minify(files, path_info, folder, optimize_css, optimize_js, def minify(files, path_info, folder, optimize_css, optimize_js,
ignore_concat=[], ignore_concat=[],
ignore_minify=['/jquery.js', '/anytime.js']): ignore_minify=['/jquery.js', '/anytime.js']):
@@ -109,7 +119,7 @@ def minify(files, path_info, folder, optimize_css, optimize_js,
js.append(contents) js.append(contents)
else: else:
js.append(filename) js.append(filename)
dest_key = hashlib.md5(repr(processed)).hexdigest() dest_key = hashlib_md5(repr(processed)).hexdigest()
if css and concat_css: if css and concat_css:
css = '\n\n'.join(contents for contents in css) css = '\n\n'.join(contents for contents in css)
if not inline_css: if not inline_css:
+3 -3
View File
@@ -105,14 +105,14 @@ class ServerProxy(object):
def __getattr__(self, attr): def __getattr__(self, attr):
"pseudo method that can be called" "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)" "JSON RPC communication (method invocation)"
# build data sent to the service # build data sent to the service
request_id = random.randint(0, sys.maxsize) 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: if self.version:
data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600 data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600
request = json.dumps(data) request = json.dumps(data)
+2 -2
View File
@@ -43,6 +43,7 @@ class WebClient(object):
self.forms = {} self.forms = {}
self.history = [] self.history = []
self.cookies = {} self.cookies = {}
self.cookiejar = cookielib.CookieJar()
self.default_headers = default_headers self.default_headers = default_headers
self.sessions = {} self.sessions = {}
self.session_regex = session_regex and re.compile(session_regex) self.session_regex = session_regex and re.compile(session_regex)
@@ -79,9 +80,8 @@ class WebClient(object):
cookies = cookies or {} cookies = cookies or {}
headers = headers or {} headers = headers or {}
cj = cookielib.CookieJar()
args = [ args = [
urllib2.HTTPCookieProcessor(cj), urllib2.HTTPCookieProcessor(self.cookiejar),
urllib2.HTTPHandler(debuglevel=0) urllib2.HTTPHandler(debuglevel=0)
] ]
# if required do basic auth # 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: try:
oname = name if not name.startswith('.') else '.'+name oname = name if not name.startswith('.') else '.'+name
return NATIVE_IMPORTER(oname, globals, locals, fromlist, level) return NATIVE_IMPORTER(oname, globals, locals, fromlist, level)
except ImportError: except (ImportError, KeyError):
items = current.request.folder.split(os.path.sep) items = current.request.folder.split(os.path.sep)
if not items[-1]: if not items[-1]:
items = 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] import_tb = sys.exc_info()[2]
try: try:
return NATIVE_IMPORTER(name, globals, locals, fromlist, level) 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 raise ImportError(e1, import_tb) # there an import error in the module
except Exception as e2: except Exception as e2:
raise # there is an error in the module raise # there is an error in the module
+8 -8
View File
@@ -14,6 +14,11 @@ from pydal import DAL as DAL
from pydal import Field from pydal import Field
from pydal.objects import Row, Rows, Table, Query, Set, Expression from pydal.objects import Row, Rows, Table, Query, Set, Expression
from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon
from gluon.serializers import custom_json, xml
from gluon.utils import web2py_uuid
from gluon import sqlhtml
from pydal.drivers import DRIVERS
def _default_validators(db, field): def _default_validators(db, field):
""" """
@@ -78,14 +83,10 @@ def _default_validators(db, field):
if (field.notnull or field.unique) and field_type not in excluded_fields: if (field.notnull or field.unique) and field_type not in excluded_fields:
requires.insert(0, validators.IS_NOT_EMPTY()) requires.insert(0, validators.IS_NOT_EMPTY())
elif not field.notnull and not field.unique and requires: elif not field.notnull and not field.unique and requires:
requires[0] = validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) requires[0] = \
validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None)
return requires return requires
from gluon.serializers import custom_json, xml
from gluon.utils import web2py_uuid
from gluon import sqlhtml
DAL.serializers = {'json': custom_json, 'xml': xml} DAL.serializers = {'json': custom_json, 'xml': xml}
DAL.validators_method = _default_validators DAL.validators_method = _default_validators
DAL.uuid = lambda x: web2py_uuid() DAL.uuid = lambda x: web2py_uuid()
@@ -96,8 +97,7 @@ DAL.representers = {
DAL.Field = Field DAL.Field = Field
DAL.Table = Table DAL.Table = Table
#: add web2py contrib drivers to pyDAL # add web2py contrib drivers to pyDAL
from pydal.drivers import DRIVERS
if not DRIVERS.get('pymysql'): if not DRIVERS.get('pymysql'):
try: try:
from .contrib import pymysql from .contrib import pymysql
+13 -13
View File
@@ -89,7 +89,7 @@ def communicate(command=None):
# New debugger implementation using dbg and a web UI # 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 from threading import RLock
interact_lock = RLock() interact_lock = RLock()
@@ -109,11 +109,11 @@ def check_interaction(fn):
return check_fn return check_fn
class WebDebugger(dbg.Frontend): class WebDebugger(c_dbg.Frontend):
"""Qdb web2py interface""" """Qdb web2py interface"""
def __init__(self, pipe, completekey='tab', stdin=None, stdout=None): def __init__(self, pipe, completekey='tab', stdin=None, stdout=None):
dbg.Frontend.__init__(self, pipe) c_dbg.Frontend.__init__(self, pipe)
self.clear_interaction() self.clear_interaction()
def clear_interaction(self): def clear_interaction(self):
@@ -128,7 +128,7 @@ class WebDebugger(dbg.Frontend):
run_lock.acquire() run_lock.acquire()
try: try:
while self.pipe.poll(): while self.pipe.poll():
dbg.Frontend.run(self) c_dbg.Frontend.run(self)
finally: finally:
run_lock.release() run_lock.release()
@@ -149,23 +149,23 @@ class WebDebugger(dbg.Frontend):
@check_interaction @check_interaction
def do_continue(self): def do_continue(self):
dbg.Frontend.do_continue(self) c_dbg.Frontend.do_continue(self)
@check_interaction @check_interaction
def do_step(self): def do_step(self):
dbg.Frontend.do_step(self) c_dbg.Frontend.do_step(self)
@check_interaction @check_interaction
def do_return(self): def do_return(self):
dbg.Frontend.do_return(self) c_dbg.Frontend.do_return(self)
@check_interaction @check_interaction
def do_next(self): def do_next(self):
dbg.Frontend.do_next(self) c_dbg.Frontend.do_next(self)
@check_interaction @check_interaction
def do_quit(self): def do_quit(self):
dbg.Frontend.do_quit(self) c_dbg.Frontend.do_quit(self)
def do_exec(self, statement): def do_exec(self, statement):
interact_lock.acquire() interact_lock.acquire()
@@ -175,18 +175,18 @@ class WebDebugger(dbg.Frontend):
# avoid spurious interaction notifications: # avoid spurious interaction notifications:
self.set_burst(2) self.set_burst(2)
# execute the statement in the remote debugger: # execute the statement in the remote debugger:
return dbg.Frontend.do_exec(self, statement) return c_dbg.Frontend.do_exec(self, statement)
finally: finally:
interact_lock.release() interact_lock.release()
# create the connection between threads: # create the connection between threads:
parent_queue, child_queue = Queue.Queue(), Queue.Queue() parent_queue, child_queue = Queue.Queue(), Queue.Queue()
front_conn = dbg.QueuePipe("parent", parent_queue, child_queue) front_conn = c_dbg.QueuePipe("parent", parent_queue, child_queue)
child_conn = dbg.QueuePipe("child", child_queue, parent_queue) child_conn = c_dbg.QueuePipe("child", child_queue, parent_queue)
web_debugger = WebDebugger(front_conn) # frontend 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 dbg = dbg_debugger
# enable getting context (stack, globals/locals) at interaction # enable getting context (stack, globals/locals) at interaction
+14 -15
View File
@@ -9,19 +9,19 @@ Based on http://code.activestate.com/recipes/52257/
Licensed under the PSF License Licensed under the PSF License
""" """
from gluon._compat import to_unicode
import codecs import codecs
# None represents a potentially variable byte. "##" in the XML spec... # None represents a potentially variable byte. "##" in the XML spec...
autodetect_dict = { # bytepattern : ("name", autodetect_dict = { # bytepattern : ("name",
(0x00, 0x00, 0xFE, 0xFF): ("ucs4_be"), (0x00, 0x00, 0xFE, 0xFF): ("ucs4_be"),
(0xFF, 0xFE, 0x00, 0x00): ("ucs4_le"), (0xFF, 0xFE, 0x00, 0x00): ("ucs4_le"),
(0xFE, 0xFF, None, None): ("utf_16_be"), (0xFE, 0xFF, None, None): ("utf_16_be"),
(0xFF, 0xFE, None, None): ("utf_16_le"), (0xFF, 0xFE, None, None): ("utf_16_le"),
(0x00, 0x3C, 0x00, 0x3F): ("utf_16_be"), (0x00, 0x3C, 0x00, 0x3F): ("utf_16_be"),
(0x3C, 0x00, 0x3F, 0x00): ("utf_16_le"), (0x3C, 0x00, 0x3F, 0x00): ("utf_16_le"),
(0x3C, 0x3F, 0x78, 0x6D): ("utf_8"), (0x3C, 0x3F, 0x78, 0x6D): ("utf_8"),
(0x4C, 0x6F, 0xA7, 0x94): ("EBCDIC") (0x4C, 0x6F, 0xA7, 0x94): ("EBCDIC")
} }
@@ -36,10 +36,10 @@ def autoDetectXMLEncoding(buffer):
# buffer at once but otherwise we'd have to decode a character at # buffer at once but otherwise we'd have to decode a character at
# a time looking for the quote character...that's a pain # a time looking for the quote character...that's a pain
encoding = "utf_8" # according to the XML spec, this is the default encoding = "utf_8"
# this code successively tries to refine the default # according to the XML spec, this is the default this code successively tries to refine the default
# whenever it fails to refine, it falls back to # whenever it fails to refine, it falls back to the last place encoding was set.
# the last place encoding was set.
if len(buffer) >= 4: if len(buffer) >= 4:
bytes = (byte1, byte2, byte3, byte4) = tuple(map(ord, buffer[0:4])) bytes = (byte1, byte2, byte3, byte4) = tuple(map(ord, buffer[0:4]))
enc_info = autodetect_dict.get(bytes, None) enc_info = autodetect_dict.get(bytes, None)
@@ -51,8 +51,7 @@ def autoDetectXMLEncoding(buffer):
enc_info = None enc_info = None
if enc_info: if enc_info:
encoding = enc_info # we've got a guess... these are encoding = enc_info # we've got a guess... these are the new defaults
#the new defaults
# try to find a more precise encoding using xml declaration # try to find a more precise encoding using xml declaration
secret_decoder_ring = codecs.lookup(encoding)[1] secret_decoder_ring = codecs.lookup(encoding)[1]
@@ -77,4 +76,4 @@ def autoDetectXMLEncoding(buffer):
def decoder(buffer): def decoder(buffer):
encoding = autoDetectXMLEncoding(buffer) encoding = autoDetectXMLEncoding(buffer)
return buffer.decode(encoding).encode('utf8') return to_unicode(buffer, charset=encoding)
+2 -2
View File
@@ -415,10 +415,10 @@ def fix_newlines(path):
|\r| |\r|
)''') )''')
for filename in listdir(path, '.*\.(py|html)$', drop=False): for filename in listdir(path, '.*\.(py|html)$', drop=False):
rdata = read_file(filename, 'rb') rdata = read_file(filename, 'r')
wdata = regex.sub('\n', rdata) wdata = regex.sub('\n', rdata)
if wdata != rdata: if wdata != rdata:
write_file(filename, wdata, 'wb') write_file(filename, wdata, 'w')
def copystream( def copystream(
+23 -20
View File
@@ -13,7 +13,8 @@ Contains the classes for the global used variables:
- Session - Session
""" """
from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \
unicodeT, long, hashlib_md5
from gluon.storage import Storage, List from gluon.storage import Storage, List
from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
from gluon.contenttype import contenttype from gluon.contenttype import contenttype
@@ -30,7 +31,7 @@ from gluon.fileutils import copystream
import hashlib import hashlib
from pydal.contrib import portalocker from pydal.contrib import portalocker
from pickle import Pickler, MARK, DICT, EMPTY_DICT from pickle import Pickler, MARK, DICT, EMPTY_DICT
#from types import DictionaryType # from types import DictionaryType
import datetime import datetime
import re import re
import os import os
@@ -48,7 +49,7 @@ PAST = 'Sat, 1-Jan-1971 00:00:00'
FUTURE = 'Tue, 1-Dec-2999 23:59:59' FUTURE = 'Tue, 1-Dec-2999 23:59:59'
try: try:
#FIXME PY3 # FIXME PY3
from gluon.contrib.minify import minify from gluon.contrib.minify import minify
have_minify = True have_minify = True
except ImportError: except ImportError:
@@ -79,6 +80,7 @@ template_mapping = {
'js:inline': js_inline 'js:inline': js_inline
} }
# IMPORTANT: # IMPORTANT:
# this is required so that pickled dict(s) and class.__dict__ # this is required so that pickled dict(s) and class.__dict__
# are sorted and web2py can detect without ambiguity when a session changes # are sorted and web2py can detect without ambiguity when a session changes
@@ -89,9 +91,11 @@ class SortingPickler(Pickler):
self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) self._batch_setitems([(key, obj[key]) for key in sorted(obj)])
if PY2: if PY2:
#FIXME PY3
SortingPickler.dispatch = copy.copy(Pickler.dispatch) SortingPickler.dispatch = copy.copy(Pickler.dispatch)
SortingPickler.dispatch[dict] = SortingPickler.save_dict 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): def sorting_dumps(obj, protocol=None):
@@ -119,7 +123,7 @@ def copystream_progress(request, chunk_size=10 ** 5):
dest = tempfile.NamedTemporaryFile() dest = tempfile.NamedTemporaryFile()
except NotImplementedError: # and GAE this except NotImplementedError: # and GAE this
dest = tempfile.TemporaryFile() dest = tempfile.TemporaryFile()
if not 'X-Progress-ID' in request.get_vars: if 'X-Progress-ID' not in request.get_vars:
copystream(source, dest, size, chunk_size) copystream(source, dest, size, chunk_size)
return dest return dest
cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID'] cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID']
@@ -197,7 +201,8 @@ class Request(Storage):
"""Takes the QUERY_STRING and unpacks it to get_vars """Takes the QUERY_STRING and unpacks it to get_vars
""" """
query_string = self.env.get('query_string', '') query_string = self.env.get('query_string', '')
dget = urlparse.parse_qs(query_string, keep_blank_values=1) # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs dget = urlparse.parse_qs(query_string, keep_blank_values=1)
# Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs
get_vars = self._get_vars = Storage(dget) get_vars = self._get_vars = Storage(dget)
for (key, value) in iteritems(get_vars): for (key, value) in iteritems(get_vars):
if isinstance(value, list) and len(value) == 1: if isinstance(value, list) and len(value) == 1:
@@ -227,8 +232,7 @@ class Request(Storage):
body.seek(0) body.seek(0)
# parse POST variables on POST, PUT, BOTH only in post_vars # parse POST variables on POST, PUT, BOTH only in post_vars
if (body and not is_json if body and not is_json and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH'):
and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH')):
query_string = env.pop('QUERY_STRING', None) query_string = env.pop('QUERY_STRING', None)
dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1) dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1)
try: try:
@@ -349,14 +353,14 @@ class Request(Storage):
current.session.forget() current.session.forget()
redirect(URL(scheme='https', args=self.args, vars=self.vars)) 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 wrapper(action, request=self):
def f(_action=action, *a, **b): def f(_action=action, *a, **b):
request.is_restful = True request.is_restful = True
env = request.env env = request.env
is_json = env.content_type=='application/json' is_json = env.content_type == 'application/json'
method = env.request_method 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('.') request.args[-1], _, request.extension = request.args[-1].rpartition('.')
current.response.headers['Content-Type'] = \ current.response.headers['Content-Type'] = \
contenttype('.' + request.extension.lower()) contenttype('.' + request.extension.lower())
@@ -415,7 +419,6 @@ class Response(Storage):
if not escape: if not escape:
self.body.write(str(data)) self.body.write(str(data))
else: else:
# FIXME PY3:
self.body.write(to_native(xmlescape(data))) self.body.write(to_native(xmlescape(data)))
def render(self, *a, **b): def render(self, *a, **b):
@@ -451,13 +454,13 @@ class Response(Storage):
for meta in iteritems((self.meta or {})): for meta in iteritems((self.meta or {})):
k, v = meta k, v = meta
if isinstance(v, dict): if isinstance(v, dict):
s += '<meta' + ''.join(' %s="%s"' % (xmlescape(key), to_native(xmlescape(v[key]))) for key in v) +' />\n' s += '<meta' + ''.join(' %s="%s"' % (xmlescape(key),
to_native(xmlescape(v[key]))) for key in v) + ' />\n'
else: else:
s += '<meta name="%s" content="%s" />\n' % (k, to_native(xmlescape(v))) s += '<meta name="%s" content="%s" />\n' % (k, to_native(xmlescape(v)))
self.write(s, escape=False) self.write(s, escape=False)
def include_files(self, extensions=None): def include_files(self, extensions=None):
""" """
Includes files (usually in the head). Includes files (usually in the head).
Can minify and cache local files Can minify and cache local files
@@ -484,8 +487,7 @@ class Response(Storage):
if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
# cache for 5 minutes by default # cache for 5 minutes by default
key = hashlib.md5(repr(files)).hexdigest() key = hashlib_md5(repr(files)).hexdigest()
cache = self.cache_includes or (current.cache.ram, 60 * 5) cache = self.cache_includes or (current.cache.ram, 60 * 5)
def call_minify(files=files): def call_minify(files=files):
@@ -523,6 +525,7 @@ class Response(Storage):
tmpl = template_mapping.get(f) tmpl = template_mapping.get(f)
if tmpl: if tmpl:
s.append(tmpl % item[1]) s.append(tmpl % item[1])
self.write(''.join(s), escape=False) self.write(''.join(s), escape=False)
def stream(self, def stream(self,
@@ -578,9 +581,9 @@ class Response(Storage):
if hasattr(stream, 'name'): if hasattr(stream, 'name'):
filename = stream.name filename = stream.name
if filename and not 'content-type' in keys: if filename and 'content-type' not in keys:
headers['Content-Type'] = contenttype(filename) headers['Content-Type'] = contenttype(filename)
if filename and not 'content-length' in keys: if filename and 'content-length' not in keys:
try: try:
headers['Content-Length'] = \ headers['Content-Length'] = \
os.path.getsize(filename) os.path.getsize(filename)
@@ -1022,7 +1025,7 @@ class Session(Storage):
if self._forget: if self._forget:
del rcookies[response.session_id_name] del rcookies[response.session_id_name]
return return
if self.get('httponly_cookies',True): if self.get('httponly_cookies', True):
scookies['HttpOnly'] = True scookies['HttpOnly'] = True
if self._secure: if self._secure:
scookies['secure'] = True scookies['secure'] = True
@@ -1193,7 +1196,7 @@ class Session(Storage):
if (not response.session_id or if (not response.session_id or
not response.session_filename or not response.session_filename or
self._forget self._forget
or self._unchanged(response)): or self._unchanged(response)):
# self.clear_session_cookies() # self.clear_session_cookies()
return False return False
else: else:
+13 -13
View File
@@ -182,8 +182,8 @@ class Highlighter(object):
)), )),
'PYTHONMultilineString': (python_tokenizer, 'PYTHONMultilineString': (python_tokenizer,
(('ENDMULTILINESTRING', (('ENDMULTILINESTRING',
re.compile(r'.*?("""|\'\'\')', re.compile(r'.*?("""|\'\'\')',
re.DOTALL), 'color: darkred'), )), re.DOTALL), 'color: darkred'), )),
'HTML': (html_tokenizer, ( 'HTML': (html_tokenizer, (
('GOTOPYTHON', re.compile(r'\{\{'), 'color: red'), ('GOTOPYTHON', re.compile(r'\{\{'), 'color: red'),
('COMMENT', re.compile(r'<!--[^>]*-->|<!>'), ('COMMENT', re.compile(r'<!--[^>]*-->|<!>'),
@@ -209,7 +209,7 @@ class Highlighter(object):
mode = self.mode mode = self.mode
while i < len(data): while i < len(data):
for (token, o_re, style) in Highlighter.all_styles[mode][1]: for (token, o_re, style) in Highlighter.all_styles[mode][1]:
if not token in self.suppress_tokens: if token not in self.suppress_tokens:
match = o_re.match(data, i) match = o_re.match(data, i)
if match: if match:
if style: if style:
@@ -221,7 +221,7 @@ class Highlighter(object):
new_mode = \ new_mode = \
Highlighter.all_styles[mode][0](self, Highlighter.all_styles[mode][0](self,
token, match, style) token, match, style)
if not new_mode is None: if new_mode is not None:
mode = new_mode mode = new_mode
i += max(1, len(match.group())) i += max(1, len(match.group()))
break break
@@ -241,9 +241,9 @@ class Highlighter(object):
style = self.styles[token] style = self.styles[token]
if self.span_style != style: if self.span_style != style:
if style != 'Keep': if style != 'Keep':
if not self.span_style is None: if self.span_style is not None:
self.output.append('</span>') self.output.append('</span>')
if not style is None: if style is not None:
self.output.append('<span style="%s">' % style) self.output.append('<span style="%s">' % style)
self.span_style = style self.span_style = style
@@ -260,7 +260,7 @@ def highlight(
): ):
styles = styles or {} styles = styles or {}
attributes = attributes or {} attributes = attributes or {}
if not 'CODE' in styles: if 'CODE' not in styles:
code_style = """ code_style = """
font-size: 11px; font-size: 11px;
font-family: Bitstream Vera Sans Mono,monospace; font-family: Bitstream Vera Sans Mono,monospace;
@@ -272,7 +272,7 @@ def highlight(
white-space: pre !important;\n""" white-space: pre !important;\n"""
else: else:
code_style = styles['CODE'] code_style = styles['CODE']
if not 'LINENUMBERS' in styles: if 'LINENUMBERS' not in styles:
linenumbers_style = """ linenumbers_style = """
font-size: 11px; font-size: 11px;
font-family: Bitstream Vera Sans Mono,monospace; font-family: Bitstream Vera Sans Mono,monospace;
@@ -283,7 +283,7 @@ def highlight(
color: #A0A0A0;\n""" color: #A0A0A0;\n"""
else: else:
linenumbers_style = styles['LINENUMBERS'] linenumbers_style = styles['LINENUMBERS']
if not 'LINEHIGHLIGHT' in styles: if 'LINEHIGHLIGHT' not in styles:
linehighlight_style = "background-color: #EBDDE2;" linehighlight_style = "background-color: #EBDDE2;"
else: else:
linehighlight_style = styles['LINEHIGHLIGHT'] linehighlight_style = styles['LINEHIGHLIGHT']
@@ -333,8 +333,9 @@ def highlight(
== '_' and value]) == '_' and value])
if fa: if fa:
fa = ' ' + fa fa = ' ' + fa
return '<table%s><tr style="vertical-align:top;"><td style="min-width:40px; text-align: right;"><pre style="%s">%s</pre></td><td><pre style="%s">%s</pre></td></tr></table>'\ return '<table%s><tr style="vertical-align:top;">' \
% (fa, linenumbers_style, numbers, code_style, code) '<td style="min-width:40px; text-align: right;"><pre style="%s">%s</pre></td>' \
'<td><pre style="%s">%s</pre></td></tr></table>' % (fa, linenumbers_style, numbers, code_style, code)
if __name__ == '__main__': if __name__ == '__main__':
@@ -342,5 +343,4 @@ if __name__ == '__main__':
argfp = open(sys.argv[1]) argfp = open(sys.argv[1])
data = argfp.read() data = argfp.read()
argfp.close() argfp.close()
print('<html><body>' + highlight(data, sys.argv[2])\ print('<html><body>' + highlight(data, sys.argv[2]) + '</body></html>')
+ '</body></html>')
+29 -40
View File
@@ -20,7 +20,8 @@ import urllib
import base64 import base64
from gluon import sanitizer, decoder from gluon import sanitizer, decoder
import itertools 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 from gluon.utils import local_html_escape
import marshal import marshal
@@ -109,6 +110,7 @@ __all__ = [
DEFAULT_PASSWORD_DISPLAY = '*' * 8 DEFAULT_PASSWORD_DISPLAY = '*' * 8
def xmlescape(data, quote=True): def xmlescape(data, quote=True):
""" """
Returns an escaped string of the provided data Returns an escaped string of the provided data
@@ -124,10 +126,9 @@ def xmlescape(data, quote=True):
if not(isinstance(data, (text_type, bytes))): if not(isinstance(data, (text_type, bytes))):
# i.e., integers # i.e., integers
data=str(data) data = str(data)
data = to_bytes(data, 'utf8', 'xmlcharrefreplace') data = to_bytes(data, 'utf8', 'xmlcharrefreplace')
# ... and do the escaping # ... and do the escaping
data = local_html_escape(data, quote) data = local_html_escape(data, quote)
return data return data
@@ -596,10 +597,10 @@ class XML(XmlComponent):
for A, IMG and BlockQuote). for A, IMG and BlockQuote).
The key is the tag; the value is a list of allowed attributes. 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): if isinstance(text, unicodeT):
text = to_native(text.encode('utf8', 'xmlcharrefreplace')) text = to_native(text.encode('utf8', 'xmlcharrefreplace'))
if sanitize:
text = sanitizer.sanitize(text, permitted_tags, allowed_attributes)
elif isinstance(text, bytes): elif isinstance(text, bytes):
text = to_native(text) text = to_native(text)
elif not isinstance(text, str): elif not isinstance(text, str):
@@ -671,6 +672,7 @@ def XML_pickle(data):
return XML_unpickle, (marshal.dumps(str(data)),) return XML_unpickle, (marshal.dumps(str(data)),)
copyreg.pickle(XML, XML_pickle, XML_unpickle) copyreg.pickle(XML, XML_pickle, XML_unpickle)
@implements_bool @implements_bool
class DIV(XmlComponent): class DIV(XmlComponent):
""" """
@@ -998,9 +1000,9 @@ class DIV(XmlComponent):
if isinstance(c, XmlComponent): if isinstance(c, XmlComponent):
s = c.flatten(render) s = c.flatten(render)
elif render: elif render:
s = render(str(c)) s = render(to_native(c))
else: else:
s = str(c) s = to_native(c)
text += s text += s
if render: if render:
text = render(text, self.tag, self.attributes) text = render(text, self.tag, self.attributes)
@@ -1281,7 +1283,6 @@ class __TAG__(XmlComponent):
def __getattr__(self, name): def __getattr__(self, name):
if name[-1:] == '_': if name[-1:] == '_':
name = name[:-1] + '/' name = name[:-1] + '/'
name=to_bytes(name)
return lambda *a, **b: __tag_div__(name, *a, **b) return lambda *a, **b: __tag_div__(name, *a, **b)
def __call__(self, html): def __call__(self, html):
@@ -1310,8 +1311,10 @@ class HTML(DIV):
tag = b'html' tag = b'html'
strict = b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n' strict = b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n'
transitional = b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' transitional = \
frameset = b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">\n' b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n'
frameset = \
b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">\n'
html5 = b'<!DOCTYPE HTML>\n' html5 = b'<!DOCTYPE HTML>\n'
def xml(self): def xml(self):
@@ -1862,7 +1865,7 @@ class INPUT(DIV):
except: except:
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
msg = "Validation error, field:%s %s" % (name,validator) msg = "Validation error, field:%s %s" % (name, validator)
raise Exception(msg) raise Exception(msg)
if errors is not None: if errors is not None:
self.vars[name] = value self.vars[name] = value
@@ -1912,7 +1915,7 @@ class INPUT(DIV):
name = self.attributes.get('_name', None) name = self.attributes.get('_name', None)
if name and hasattr(self, 'errors') \ if name and hasattr(self, 'errors') \
and self.errors.get(name, None) \ and self.errors.get(name, None) \
and self['hideerror'] != True: and self['hideerror'] is not True:
self['_class'] = (self['_class'] and self['_class'] + ' ' or '') + 'invalidinput' self['_class'] = (self['_class'] and self['_class'] + ' ' or '') + 'invalidinput'
return DIV.xml(self) + DIV( return DIV.xml(self) + DIV(
DIV( DIV(
@@ -1980,7 +1983,6 @@ class OPTGROUP(DIV):
class SELECT(INPUT): class SELECT(INPUT):
""" """
Examples: Examples:
@@ -2015,7 +2017,7 @@ class SELECT(INPUT):
if value is not None: if value is not None:
if not self['_multiple']: if not self['_multiple']:
for c in options: # my patch for c in options: # my patch
if ((value is not None) and (str(c['_value']) == str(value))): if (value is not None) and (str(c['_value']) == str(value)):
c['_selected'] = 'selected' c['_selected'] = 'selected'
else: else:
c['_selected'] = None c['_selected'] = None
@@ -2025,7 +2027,7 @@ class SELECT(INPUT):
else: else:
values = [str(value)] values = [str(value)]
for c in options: # my patch for c in options: # my patch
if ((value is not None) and (str(c['_value']) in values)): if (value is not None) and (str(c['_value']) in values):
c['_selected'] = 'selected' c['_selected'] = 'selected'
else: else:
c['_selected'] = None c['_selected'] = None
@@ -2376,22 +2378,21 @@ class FORM(DIV):
def as_json(self, sanitize=True): def as_json(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize) d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import json from gluon.serializers import json
return json(d) return json(d)
def as_yaml(self, sanitize=True): def as_yaml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize) d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import yaml from gluon.serializers import yaml
return yaml(d) return yaml(d)
def as_xml(self, sanitize=True): def as_xml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize) d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import xml from gluon.serializers import xml
return xml(d) return xml(d)
class BEAUTIFY(DIV): class BEAUTIFY(DIV):
""" """
Turns any list, dictionary, etc into decent looking html. Turns any list, dictionary, etc into decent looking html.
@@ -2548,7 +2549,7 @@ class MENU(DIV):
li['_class'] = li['_class'] + ' ' + self['li_active'] li['_class'] = li['_class'] + ' ' + self['li_active']
else: else:
li['_class'] = self['li_active'] li['_class'] = self['li_active']
if len(item) <= 4 or item[4] == True: if len(item) <= 4 or item[4] is True:
ul.append(li) ul.append(li)
return ul return ul
@@ -2562,7 +2563,7 @@ class MENU(DIV):
# ex: ('', False, A('title', _href=URL(...), _title="title")) # ex: ('', False, A('title', _href=URL(...), _title="title"))
# ex: (A('title', _href=URL(...), _title="title"), False, None) # ex: (A('title', _href=URL(...), _title="title"), False, None)
custom_items.append(item) custom_items.append(item)
elif len(item) <= 4 or item[4] == True: elif len(item) <= 4 or item[4] is True:
select.append(OPTION(CAT(prefix, item[0]), select.append(OPTION(CAT(prefix, item[0]),
_value=item[2], _selected=item[1])) _value=item[2], _selected=item[1]))
if len(item) > 3 and len(item[3]): if len(item) > 3 and len(item[3]):
@@ -2655,36 +2656,24 @@ class web2pyHTMLParser(HTMLParser):
""" """
obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers. obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers.
obj.tree contains the root of the tree, and tree can be manipulated 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')): def __init__(self, text, closed=('input', 'link')):
HTMLParser.__init__(self) HTMLParser.__init__(self)
self.tree = self.parent = TAG['']() self.tree = self.parent = TAG['']()
self.closed = closed self.closed = closed
self.tags = [x for x in __all__ if isinstance(eval(x), DIV)]
self.last = None self.last = None
self.feed(text) self.feed(text)
def handle_starttag(self, tagname, attrs): def handle_starttag(self, tagname, attrs):
if tagname.upper() in self.tags: if tagname in self.closed:
tag = eval(tagname.upper()) tagname += '/'
else: tag = TAG[tagname]()
if tagname in self.closed:
tagname += '/'
tag = TAG[tagname]()
for key, value in attrs: for key, value in attrs:
tag['_' + key] = value tag['_' + key] = value
tag.parent = self.parent tag.parent = self.parent
self.parent.append(tag) self.parent.append(tag)
if not tag.tag.endswith(b'/'): if not tag.tag.endswith('/'):
self.parent = tag self.parent = tag
else: else:
self.last = tag.tag[:-1] self.last = tag.tag[:-1]
@@ -2707,7 +2696,6 @@ class web2pyHTMLParser(HTMLParser):
self.parent.append(entitydefs[name]) self.parent.append(entitydefs[name])
def handle_endtag(self, tagname): def handle_endtag(self, tagname):
tagname = to_bytes(tagname)
# this deals with unbalanced tags # this deals with unbalanced tags
if tagname == self.last: if tagname == self.last:
return return
@@ -2717,7 +2705,8 @@ class web2pyHTMLParser(HTMLParser):
self.parent = self.parent.parent self.parent = self.parent.parent
except: except:
raise RuntimeError("unable to balance tag %s" % tagname) raise RuntimeError("unable to balance tag %s" % tagname)
if parent_tagname[:len(tagname)] == tagname: break if parent_tagname[:len(tagname)] == tagname:
break
def markdown_serializer(text, tag=None, attr=None): def markdown_serializer(text, tag=None, attr=None):
+5 -3
View File
@@ -11,7 +11,7 @@ HTTP statuses helpers
""" """
import re import re
from gluon._compat import iteritems from gluon._compat import iteritems, unicodeT
__all__ = ['HTTP', 'redirect'] __all__ = ['HTTP', 'redirect']
@@ -116,12 +116,14 @@ class HTTP(Exception):
for k, v in iteritems(headers): for k, v in iteritems(headers):
if isinstance(v, list): if isinstance(v, list):
rheaders += [(k, str(item)) for item in v] rheaders += [(k, str(item)) for item in v]
elif not v is None: elif v is not None:
rheaders.append((k, str(v))) rheaders.append((k, str(v)))
responder(status, rheaders) responder(status, rheaders)
if env.get('request_method', '') == 'HEAD': if env.get('request_method', '') == 'HEAD':
return [''] return ['']
elif isinstance(body, (str, bytes, bytearray)): elif isinstance(body, (str, bytes, bytearray)):
if isinstance(body, unicodeT):
body = body.encode('utf-8')
return [body] return [body]
elif hasattr(body, '__iter__'): elif hasattr(body, '__iter__'):
return body return body
@@ -148,7 +150,7 @@ class HTTP(Exception):
web2py_error=self.headers.get('web2py_error')) web2py_error=self.headers.get('web2py_error'))
def __str__(self): def __str__(self):
"stringify me" """stringify me"""
return self.message return self.message
+1 -5
View File
@@ -78,13 +78,9 @@ alert_dependency = ['hashlib', 'uuid']
# Now we remove the blacklisted modules if we are using the stated # Now we remove the blacklisted modules if we are using the stated
# python version. # python version.
# #
# List of modules deprecated in Python 2.6 or 2.7 that are in the above set # List of modules deprecated in Python 2.7 that are in the above list
py27_deprecated = ['mhlib', 'multifile', 'mimify', 'sets', 'MimeWriter'] # And ['optparse'] but we need it for now py27_deprecated = ['mhlib', 'multifile', 'mimify', 'sets', 'MimeWriter'] # And ['optparse'] but we need it for now
if python_version >= '2.6':
base_modules += ['json', 'multiprocessing']
base_modules = list(set(base_modules).difference(set(py26_deprecated)))
if python_version >= '2.7': if python_version >= '2.7':
base_modules += ['argparse', 'json', 'multiprocessing'] base_modules += ['argparse', 'json', 'multiprocessing']
base_modules = list(set(base_modules).difference(set(py27_deprecated))) base_modules = list(set(base_modules).difference(set(py27_deprecated)))
+18 -10
View File
@@ -951,32 +951,40 @@ def findT(path, language=DEFAULT_LANGUAGE):
Note: Note:
Must be run by the admin app Must be run by the admin app
""" """
from gluon.tools import Auth, Crud
lang_file = pjoin(path, 'languages', language + '.py') lang_file = pjoin(path, 'languages', language + '.py')
sentences = read_dict(lang_file) sentences = read_dict(lang_file)
mp = pjoin(path, 'models') mp = pjoin(path, 'models')
cp = pjoin(path, 'controllers') cp = pjoin(path, 'controllers')
vp = pjoin(path, 'views') vp = pjoin(path, 'views')
mop = pjoin(path, 'modules') mop = pjoin(path, 'modules')
def add_message(message):
if not message.startswith('#') and not '\n' in message:
tokens = message.rsplit('##', 1)
else:
# this allows markmin syntax in translations
tokens = [message]
if len(tokens) == 2:
message = tokens[0].strip() + '##' + tokens[1].strip()
if message and not message in sentences:
sentences[message] = message.replace("@markmin\x01", "")
for filename in \ for filename in \
listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\ listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\
+ listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0):
data = to_native(read_locked(filename)) data = to_native(read_locked(filename))
items = regex_translate.findall(data) items = regex_translate.findall(data)
items += ["@markmin\x01%s" %x for x in regex_translate_m.findall(data)] for x in regex_translate_m.findall(data):
if x[0:3] in ["'''", '"""']: items.append("%s@markmin\x01%s" %(x[0:3], x[3:]))
else: items.append("%s@markmin\x01%s" %(x[0], x[1:]))
for item in items: for item in items:
try: try:
message = safe_eval(item) message = safe_eval(item)
except: except:
continue # silently ignore inproperly formatted strings continue # silently ignore inproperly formatted strings
if not message.startswith('#') and not '\n' in message: add_message(message)
tokens = message.rsplit('##', 1) gluon_msg = [Auth.default_messages, Crud.default_messages]
else: for item in [x for m in gluon_msg for x in m.values() if x is not None]:
# this allows markmin syntax in translations add_message(item)
tokens = [message]
if len(tokens) == 2:
message = tokens[0].strip() + '##' + tokens[1].strip()
if message and not message in sentences:
sentences[message] = message.replace("@markmin\x01", "")
if not '!langcode!' in sentences: if not '!langcode!' in sentences:
sentences['!langcode!'] = ( sentences['!langcode!'] = (
DEFAULT_LANGUAGE if language in ('default', DEFAULT_LANGUAGE) else language) DEFAULT_LANGUAGE if language in ('default', DEFAULT_LANGUAGE) else language)
+20 -21
View File
@@ -11,7 +11,8 @@ The gluon wsgi application
""" """
from __future__ import print_function from __future__ import print_function
if False: import import_all # DO NOT REMOVE PART OF FREEZE PROCESS if False:
import import_all # DO NOT REMOVE PART OF FREEZE PROCESS
import gc import gc
import os import os
@@ -26,7 +27,7 @@ import random
import string import string
from gluon._compat import Cookie, urllib2 from gluon._compat import Cookie, urllib2
#from thread import allocate_lock # from thread import allocate_lock
from gluon.fileutils import abspath, write_file from gluon.fileutils import abspath, write_file
from gluon.settings import global_settings from gluon.settings import global_settings
@@ -67,14 +68,14 @@ import gluon.messageboxhandler
logging.gluon = gluon logging.gluon = gluon
# so we must restore it! Thanks ozancag # so we must restore it! Thanks ozancag
import locale import locale
locale.setlocale(locale.LC_CTYPE, "C") # IMPORTANT, web2py requires locale "C" locale.setlocale(locale.LC_CTYPE, "C") # IMPORTANT, web2py requires locale "C"
exists = os.path.exists exists = os.path.exists
pjoin = os.path.join pjoin = os.path.join
try: try:
logging.config.fileConfig(abspath("logging.conf")) logging.config.fileConfig(abspath("logging.conf"))
except: # fails on GAE or when logfile is missing except: # fails on GAE or when logfile is missing
logging.basicConfig() logging.basicConfig()
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
@@ -254,6 +255,7 @@ class LazyWSGI(object):
return [data] return [data]
for item in middleware_apps: for item in middleware_apps:
app = item(app) app = item(app)
def caller(app): def caller(app):
return app(self.environ, self.start_response) return app(self.environ, self.start_response)
return lambda caller=caller, app=app: caller(app) return lambda caller=caller, app=app: caller(app)
@@ -294,9 +296,9 @@ def wsgibase(environ, responder):
response = Response() response = Response()
session = Session() session = Session()
env = request.env env = request.env
#env.web2py_path = global_settings.applications_parent # env.web2py_path = global_settings.applications_parent
env.web2py_version = web2py_version env.web2py_version = web2py_version
#env.update(global_settings) # env.update(global_settings)
static_file = False static_file = False
http_response = None http_response = None
try: try:
@@ -325,7 +327,6 @@ def wsgibase(environ, responder):
'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT' 'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT'
response.stream(static_file, request=request) response.stream(static_file, request=request)
# ################################################## # ##################################################
# fill in request items # fill in request items
# ################################################## # ##################################################
@@ -356,17 +357,15 @@ def wsgibase(environ, responder):
cmd_opts = global_settings.cmd_options cmd_opts = global_settings.cmd_options
request.update( request.update(
client = client, client=client,
folder = abspath('applications', app) + os.sep, folder=abspath('applications', app) + os.sep,
ajax = x_req_with == 'xmlhttprequest', ajax=x_req_with == 'xmlhttprequest',
cid = env.http_web2py_component_element, cid=env.http_web2py_component_element,
is_local = (env.remote_addr in local_hosts and is_local=(env.remote_addr in local_hosts and client == env.remote_addr),
client == env.remote_addr), is_shell=False,
is_shell = False, is_scheduler=False,
is_scheduler = False, is_https=env.wsgi_url_scheme in HTTPS_SCHEMES or
is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \ request.env.http_x_forwarded_proto in HTTPS_SCHEMES or env.https == 'on'
request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
or env.https == 'on'
) )
request.url = environ['PATH_INFO'] request.url = environ['PATH_INFO']
@@ -390,7 +389,7 @@ def wsgibase(environ, responder):
% 'invalid request', % 'invalid request',
web2py_error='invalid application') web2py_error='invalid application')
elif not request.is_local and exists(disabled): elif not request.is_local and exists(disabled):
five0three = os.path.join(request.folder,'static','503.html') five0three = os.path.join(request.folder, 'static', '503.html')
if os.path.exists(five0three): if os.path.exists(five0three):
raise HTTP(503, file(five0three, 'r').read()) raise HTTP(503, file(five0three, 'r').read())
else: else:
@@ -406,7 +405,7 @@ def wsgibase(environ, responder):
# get the GET and POST data # get the GET and POST data
# ################################################## # ##################################################
#parse_get_post_vars(request, environ) # parse_get_post_vars(request, environ)
# ################################################## # ##################################################
# expose wsgi hooks for convenience # expose wsgi hooks for convenience
@@ -625,7 +624,7 @@ def appfactory(wsgiapp=wsgibase,
raise BaseException("Can't create dir %s" % profiler_dir) raise BaseException("Can't create dir %s" % profiler_dir)
filepath = pjoin(profiler_dir, 'wtest') filepath = pjoin(profiler_dir, 'wtest')
try: try:
filehandle = open( filepath, 'w' ) filehandle = open(filepath, 'w')
filehandle.close() filehandle.close()
os.unlink(filepath) os.unlink(filepath)
except IOError: except IOError:
+9 -6
View File
@@ -19,13 +19,14 @@ import sched
import re import re
import datetime import datetime
import platform import platform
import gluon.fileutils
from functools import reduce from functools import reduce
try: try:
import cPickle as pickle import cPickle as pickle
except: except:
import pickle import pickle
from gluon.settings import global_settings from gluon.settings import global_settings
from gluon import fileutils
from gluon._compat import to_bytes
from pydal.contrib import portalocker from pydal.contrib import portalocker
logger = logging.getLogger("web2py.cron") logger = logging.getLogger("web2py.cron")
@@ -116,7 +117,7 @@ class Token(object):
def __init__(self, path): def __init__(self, path):
self.path = os.path.join(path, 'cron.master') self.path = os.path.join(path, 'cron.master')
if not os.path.exists(self.path): 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.master = None
self.now = time.time() self.now = time.time()
@@ -139,7 +140,7 @@ class Token(object):
if portalocker.LOCK_EX is None: if portalocker.LOCK_EX is None:
logger.warning('WEB2PY CRON: Disabled because no file locking') logger.warning('WEB2PY CRON: Disabled because no file locking')
return None return None
self.master = open(self.path, 'rb+') self.master = fileutils.open_file(self.path, 'rb+')
try: try:
ret = None ret = None
portalocker.lock(self.master, portalocker.LOCK_EX) 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 Writes into cron.master the time when cron job was completed
""" """
ret = self.master.closed
if not self.master.closed: if not self.master.closed:
portalocker.lock(self.master, portalocker.LOCK_EX) portalocker.lock(self.master, portalocker.LOCK_EX)
logger.debug('WEB2PY CRON: Releasing cron lock') logger.debug('WEB2PY CRON: Releasing cron lock')
@@ -177,6 +179,7 @@ class Token(object):
pickle.dump((self.now, time.time()), self.master) pickle.dump((self.now, time.time()), self.master)
portalocker.unlock(self.master) portalocker.unlock(self.master)
self.master.close() self.master.close()
return ret
def rangetolist(s, period='min'): def rangetolist(s, period='min'):
@@ -222,8 +225,8 @@ def parsecronline(line):
params = line.strip().split(None, 6) params = line.strip().split(None, 6)
if len(params) < 7: if len(params) < 7:
return None return None
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
'fri': 5, 'sat': 6} 'thu': 4, 'fri': 5, 'sat': 6}
for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']): for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
if not s in [None, '*']: if not s in [None, '*']:
task[id] = [] task[id] = []
@@ -236,7 +239,7 @@ def parsecronline(line):
elif val.isdigit() or val == '-1': elif val.isdigit() or val == '-1':
task[id].append(int(val)) task[id].append(int(val))
elif id == 'dow' and val[:3].lower() in daysofweek: 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['user'] = params[5]
task['cmd'] = params[6] task['cmd'] = params[6]
return task return task
+4 -1
View File
@@ -137,7 +137,10 @@ class RestrictedError(Exception):
self.environment = environment self.environment = environment
if layer: if layer:
try: try:
self.traceback = traceback.format_exc() try:
self.traceback = traceback.format_exc()
except:
self.traceback = traceback.format_exc(limit=1)
except: except:
self.traceback = 'no traceback because template parsing error' self.traceback = 'no traceback because template parsing error'
try: try:
+3 -4
View File
@@ -206,8 +206,7 @@ def url_out(request, environ, application, controller, function,
if host is True or (host is None and (scheme or port is not None)): if host is True or (host is None and (scheme or port is not None)):
host = request.env.http_host host = request.env.http_host
if not scheme or scheme is True: if not scheme or scheme is True:
scheme = request.env.get('wsgi_url_scheme', 'http').lower() \ scheme = request.env.get('wsgi_url_scheme', 'http').lower() if request else 'http'
if request else 'http'
if host: if host:
host_port = host if not port else host.split(':', 1)[0] + ':%s' % port host_port = host if not port else host.split(':', 1)[0] + ':%s' % port
url = '%s://%s%s' % (scheme, host_port, url) url = '%s://%s%s' % (scheme, host_port, url)
@@ -317,7 +316,7 @@ def load(routes='routes.py', app=None, data=None, rdict=None):
symbols = dict(app=app) symbols = dict(app=app)
try: try:
exec(data + '\n', symbols) exec(data, symbols)
except SyntaxError as e: except SyntaxError as e:
logger.error( logger.error(
'%s has a syntax error and will not be loaded\n' % path '%s has a syntax error and will not be loaded\n' % path
@@ -1040,7 +1039,7 @@ class MapUrlIn(object):
else: else:
default_function = self.router.default_function # str or None default_function = self.router.default_function # str or None
default_function = self.domain_function or default_function default_function = self.domain_function or default_function
if not arg0 or functions and arg0 not in functions: if not arg0 or functions and arg0.split('.')[0] not in functions:
self.function = default_function or "" self.function = default_function or ""
self.pop_arg_if(arg0 and self.function == arg0) self.pop_arg_if(arg0 and self.function == arg0)
else: else:
+4 -5
View File
@@ -307,13 +307,13 @@ try:
except ImportError: except ImportError:
has_futures = False has_futures = False
class Future: class Future(object):
pass pass
class ThreadPoolExecutor: class ThreadPoolExecutor(object):
pass pass
class _WorkItem: class _WorkItem(object):
pass pass
@@ -784,8 +784,7 @@ class Rocket(object):
the application developer. Please update your \ the application developer. Please update your \
applications to no longer call rocket.stop(True)" applications to no longer call rocket.stop(True)"
try: try:
import warnings raise DeprecationWarning(msg)
raise warnings.DeprecationWarning(msg)
except ImportError: except ImportError:
raise RuntimeError(msg) 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 gluon._compat import HTMLParser, urlparse, entitydefs, basestring
from cgi import escape from gluon.utils import local_html_escape
from formatter import AbstractFormatter from formatter import AbstractFormatter
from xml.sax.saxutils import quoteattr from xml.sax.saxutils import quoteattr
@@ -21,7 +21,7 @@ __all__ = ['sanitize']
def xssescape(text): def xssescape(text):
"""Gets rid of < and > and & and, for good measure, :""" """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): class XssCleaner(HTMLParser):
+2 -2
View File
@@ -1158,7 +1158,7 @@ class Scheduler(MetaScheduler):
if not self.db_thread: if not self.db_thread:
logger.debug('thread building own DAL object') logger.debug('thread building own DAL object')
self.db_thread = DAL( 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) self.define_tables(self.db_thread, migrate=False)
try: try:
db = self.db_thread db = self.db_thread
@@ -1698,7 +1698,7 @@ def main():
print('groups for this worker: ' + ', '.join(group_names)) print('groups for this worker: ' + ', '.join(group_names))
print('connecting to database in folder: ' + options.db_folder or './') print('connecting to database in folder: ' + options.db_folder or './')
print('using URI: ' + options.db_uri) 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...') print('instantiating scheduler...')
scheduler = Scheduler(db=db, scheduler = Scheduler(db=db,
worker_name=options.worker_name, worker_name=options.worker_name,
+39 -31
View File
@@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
from pydal.base import DEFAULT from pydal.base import DEFAULT
from pydal.objects import Table, Row, Expression, Field, Set, Rows from pydal.objects import Table, Row, Expression, Field, Set, Rows
from pydal.adapters.base import CALLABLETYPES 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 pydal.helpers.classes import Reference, SQLCustomType
from gluon.storage import Storage from gluon.storage import Storage
from gluon.utils import md5_hash from gluon.utils import md5_hash
@@ -405,7 +405,7 @@ class RadioWidget(OptionsWidget):
cols = attributes.get('cols', 1) cols = attributes.get('cols', 1)
totals = len(options) totals = len(options)
mods = totals % cols mods = totals % cols
rows = totals / cols rows = totals // cols
if mods: if mods:
rows += 1 rows += 1
@@ -471,7 +471,7 @@ class CheckboxesWidget(OptionsWidget):
cols = attributes.get('cols', 1) cols = attributes.get('cols', 1)
totals = len(options) totals = len(options)
mods = totals % cols mods = totals % cols
rows = totals / cols rows = totals // cols
if mods: if mods:
rows += 1 rows += 1
@@ -684,9 +684,10 @@ class AutocompleteWidget(object):
urlvars = request.vars urlvars = request.vars
urlvars[default_var] = 1 urlvars[default_var] = 1
self.url = URL(args=request.args, vars=urlvars) self.url = URL(args=request.args, vars=urlvars)
self.callback() self.run_callback = True
else: else:
self.url = request self.url = request
self.run_callback = False
def callback(self): def callback(self):
if self.keyword in self.request.vars: if self.keyword in self.request.vars:
@@ -759,6 +760,8 @@ class AutocompleteWidget(object):
raise HTTP(200, '') raise HTTP(200, '')
def __call__(self, field, value, **attributes): def __call__(self, field, value, **attributes):
if self.run_callback:
self.callback()
default = dict( default = dict(
_type='text', _type='text',
value=(value is not None and str(value)) or '', value=(value is not None and str(value)) or '',
@@ -1916,7 +1919,7 @@ class SQLFORM(FORM):
if 'table_name' in attributes: if 'table_name' in attributes:
del attributes['table_name'] del attributes['table_name']
return SQLFORM(DAL(None).define_table(table_name, *fields), return SQLFORM(DAL(None).define_table(table_name, *[field.clone() for field in fields]),
**attributes) **attributes)
@staticmethod @staticmethod
@@ -2153,7 +2156,8 @@ class SQLFORM(FORM):
ignore_common_filters=None, ignore_common_filters=None,
auto_pagination=True, auto_pagination=True,
use_cursor=False, use_cursor=False,
represent_none=None): represent_none=None,
showblobs=False):
formstyle = formstyle or current.response.formstyle formstyle = formstyle or current.response.formstyle
if isinstance(query, Set): if isinstance(query, Set):
@@ -2194,7 +2198,7 @@ class SQLFORM(FORM):
buttondelete='icon trash icon-trash glyphicon glyphicon-trash', buttondelete='icon trash icon-trash glyphicon glyphicon-trash',
buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil', buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil',
buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right', buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right',
buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in', buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in'
) )
elif not isinstance(ui, dict): elif not isinstance(ui, dict):
raise RuntimeError('SQLFORM.grid ui argument must be a dictionary') raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
@@ -2328,7 +2332,7 @@ class SQLFORM(FORM):
if not isinstance(left, (list, tuple)): if not isinstance(left, (list, tuple)):
left = [left] left = [left]
for join in 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] tables = [db[tablename] for tablename in tablenames]
if fields: if fields:
# add missing tablename to virtual fields # add missing tablename to virtual fields
@@ -2340,7 +2344,7 @@ class SQLFORM(FORM):
else: else:
fields = [] fields = []
columns = [] columns = []
filter1 = lambda f: isinstance(f, Field) and f.type != 'blob' filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs)
filter2 = lambda f: isinstance(f, Field) and f.readable filter2 = lambda f: isinstance(f, Field) and f.readable
for table in tables: for table in tables:
fields += filter(filter1, table) fields += filter(filter1, table)
@@ -2553,6 +2557,7 @@ class SQLFORM(FORM):
if isinstance(field, Field.Virtual) and not str(field) in expcolumns: if isinstance(field, Field.Virtual) and not str(field) in expcolumns:
expcolumns.append(str(field)) expcolumns.append(str(field))
expcolumns = ['"%s"' % '"."'.join(f.split('.')) for f in expcolumns]
if export_type in exportManager and exportManager[export_type]: if export_type in exportManager and exportManager[export_type]:
if keywords: if keywords:
try: try:
@@ -2862,7 +2867,7 @@ class SQLFORM(FORM):
for field in columns: for field in columns:
if not field.readable: if not field.readable:
continue continue
if field.type == 'blob': elif field.type == 'blob' and not showblobs:
continue continue
if isinstance(field, Field.Virtual) and field.tablename in row: if isinstance(field, Field.Virtual) and field.tablename in row:
try: try:
@@ -3150,7 +3155,9 @@ class SQLFORM(FORM):
# if isinstance(linked_tables, dict): # if isinstance(linked_tables, dict):
# linked_tables = linked_tables.get(table._tablename, []) # linked_tables = linked_tables.get(table._tablename, [])
if linked_tables is None or referee in linked_tables: 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): except (KeyError, ValueError, TypeError):
redirect(URL(args=table._tablename)) redirect(URL(args=table._tablename))
if nargs == len(args) + 1: if nargs == len(args) + 1:
@@ -3311,23 +3318,28 @@ class SQLTABLE(TABLE):
if not sqlrows: if not sqlrows:
return return
REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD 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: if not columns:
columns = list(sqlrows.colnames) 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 = {} headers = {}
for c in columns: for c in columns:
tfmatch = REGEX_TABLE_DOT_FIELD.match(c) f = fieldmap.get(c)
if tfmatch: if isinstance(f, field_types):
(t, f) = REGEX_TABLE_DOT_FIELD.match(c).groups() headers[c] = make_name(f)
headers[t + '.' + f] = f.replace('_', ' ').title()
else: else:
headers[c] = REGEX_ALIAS_MATCH.sub(r'\2', c) 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: if colgroup:
cols = [COL(_id=c.replace('.', '-'), data={'column': i + 1}) cols = [COL(_id=c.replace('.', '-'), data={'column': i + 1})
for i, c in enumerate(columns)] for i, c in enumerate(columns)]
@@ -3380,9 +3392,8 @@ class SQLTABLE(TABLE):
_class += ' rowselected' _class += ' rowselected'
for colname in columns: for colname in columns:
matched_column_field = \ field = fieldmap.get(colname)
sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD.match(colname) if not isinstance(field, field_types):
if not matched_column_field:
if "_extra" in record and colname in record._extra: if "_extra" in record and colname in record._extra:
r = record._extra[colname] r = record._extra[colname]
row.append(TD(r)) row.append(TD(r))
@@ -3390,12 +3401,9 @@ class SQLTABLE(TABLE):
else: else:
raise KeyError( raise KeyError(
"Column %s not found (SQLTABLE)" % colname) "Column %s not found (SQLTABLE)" % colname)
(tablename, fieldname) = matched_column_field.groups() # Virtual fields don't have parent table name...
colname = tablename + '.' + fieldname tablename = colname.split('.', 1)[0]
try: fieldname = field.name
field = sqlrows.db[tablename][fieldname]
except (KeyError, AttributeError):
field = None
if tablename in record \ if tablename in record \
and isinstance(record, Row) \ and isinstance(record, Row) \
and isinstance(record[tablename], Row): and isinstance(record[tablename], Row):
+3 -1
View File
@@ -13,6 +13,7 @@ from .test_contribs import *
from .test_routes import * from .test_routes import *
from .test_router import * from .test_router import *
from .test_validators import * from .test_validators import *
from .test_authapi import *
from .test_tools import * from .test_tools import *
from .test_utils import * from .test_utils import *
from .test_serializers import * from .test_serializers import *
@@ -22,7 +23,8 @@ from .test_appadmin import *
from .test_web import * from .test_web import *
from .test_sqlhtml import * from .test_sqlhtml import *
from .test_scheduler import * from .test_scheduler import *
from .test_cron import *
from .test_is_url import *
if sys.version[:3] == '2.7': if sys.version[:3] == '2.7':
from .test_is_url import *
from .test_old_doctests import * from .test_old_doctests import *
+24 -4
View File
@@ -17,6 +17,7 @@ from gluon import fileutils
from gluon.dal import DAL, Field, Table from gluon.dal import DAL, Field, Table
from gluon.http import HTTP from gluon.http import HTTP
from gluon.fileutils import open_file from gluon.fileutils import open_file
from gluon.cache import CacheInRam
DEFAULT_URI = os.getenv('DB', 'sqlite:memory') DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
@@ -91,7 +92,8 @@ class TestAppAdmin(unittest.TestCase):
self.run_view() self.run_view()
self.run_view_file_stream() self.run_view_file_stream()
except Exception as e: except Exception as e:
print(e.message) import traceback
print(traceback.format_exc())
self.fail('Could not make the view') self.fail('Could not make the view')
def test_index(self): def test_index(self):
@@ -103,6 +105,21 @@ class TestAppAdmin(unittest.TestCase):
self._test_index() self._test_index()
remove_compiled_application(appname_path) remove_compiled_application(appname_path)
def test_index_minify(self):
# test for gluon/contrib/minify
self.env['response'].optimize_css = 'concat|minify'
self.env['response'].optimize_js = 'concat|minify'
self.env['current'].cache = Storage({'ram':CacheInRam()})
appname_path = os.path.join(os.getcwd(), 'applications', 'welcome')
self._test_index()
file_l = os.listdir(os.path.join(appname_path, 'static', 'temp'))
file_l.sort()
self.assertTrue(len(file_l) == 2)
self.assertEqual(file_l[0][0:10], 'compressed')
self.assertEqual(file_l[1][0:10], 'compressed')
self.assertEqual(file_l[0][-3:], 'css')
self.assertEqual(file_l[1][-2:], 'js')
def test_select(self): def test_select(self):
request = self.env['request'] request = self.env['request']
request.args = List(['db']) request.args = List(['db'])
@@ -115,7 +132,8 @@ class TestAppAdmin(unittest.TestCase):
try: try:
self.run_view() self.run_view()
except Exception as e: except Exception as e:
print(e.message) import traceback
print(traceback.format_exc())
self.fail('Could not make the view') self.fail('Could not make the view')
def test_insert(self): def test_insert(self):
@@ -129,7 +147,8 @@ class TestAppAdmin(unittest.TestCase):
try: try:
self.run_view() self.run_view()
except Exception as e: except Exception as e:
print(e.message) import traceback
print(traceback.format_exc())
self.fail('Could not make the view') self.fail('Could not make the view')
def test_insert_submit(self): def test_insert_submit(self):
@@ -151,7 +170,8 @@ class TestAppAdmin(unittest.TestCase):
try: try:
self.run_view() self.run_view()
except Exception as e: except Exception as e:
print(e.message) import traceback
print(traceback.format_exc())
self.fail('Could not make the view') self.fail('Could not make the view')
db = self.env['db'] db = self.env['db']
lisa_record = db(db.auth_user.username == 'lisasimpson').select().first() lisa_record = db(db.auth_user.username == 'lisasimpson').select().first()
+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())
+5 -1
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import unittest import unittest
import datetime import datetime
from gluon.fileutils import parse_version from gluon.fileutils import parse_version, fix_newlines
class TestFileUtils(unittest.TestCase): class TestFileUtils(unittest.TestCase):
@@ -22,3 +23,6 @@ class TestFileUtils(unittest.TestCase):
# Semantic Beta # Semantic Beta
rtn = parse_version('Version 2.14.1-beta+timestamp.2016.03.21.22.35.26') rtn = parse_version('Version 2.14.1-beta+timestamp.2016.03.21.22.35.26')
self.assertEqual(rtn, (2, 14, 1, 'beta', datetime.datetime(2016, 3, 21, 22, 35, 26))) self.assertEqual(rtn, (2, 14, 1, 'beta', datetime.datetime(2016, 3, 21, 22, 35, 26)))
def test_fix_newlines(self):
fix_newlines(os.path.dirname(os.path.abspath(__file__)))
+72 -24
View File
@@ -10,27 +10,76 @@ import re
import unittest import unittest
from gluon.globals import Request, Response, Session from gluon.globals import Request, Response, Session
from gluon.rewrite import regex_url_in
from gluon import URL from gluon import URL
from gluon._compat import basestring from gluon._compat import basestring
def setup_clean_session(): def setup_clean_session():
request = Request(env={}) request = Request(env={})
request.application = 'a' request.application = 'a'
request.controller = 'c' request.controller = 'c'
request.function = 'f' request.function = 'f'
request.folder = 'applications/admin' request.folder = 'applications/admin'
response = Response() response = Response()
session = Session() session = Session()
session.connect(request, response) 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 from gluon.globals import current
current.request = request current.response = Response()
current.response = response
current.session = session def test_restful_simple(self):
return current 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): 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): def assertRegexpMatches(self, text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression.""" """Fail the test unless the text matches the regular expression."""
if isinstance(expected_regexp, basestring): if isinstance(expected_regexp, basestring):
@@ -99,8 +148,8 @@ class testResponse(unittest.TestCase):
response.files.append(URL('a', 'static', 'css/file.css')) response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response) content = return_includes(response)
self.assertEqual(content, self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" 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" />') '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
response = Response() response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) 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')) response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response) content = return_includes(response)
self.assertEqual(content, self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" 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" />' + '<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="/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="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
) )
response = Response() response = Response()
response.files.append(URL('a', 'static', 'css/file.js')) response.files.append(URL('a', 'static', 'css/file.js'))
@@ -122,13 +170,13 @@ class testResponse(unittest.TestCase):
content = return_includes(response, extensions=['css']) content = return_includes(response, extensions=['css'])
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/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 = Response()
response.files.append('http://maps.google.com/maps/api/js?sensor=false') response.files.append('http://maps.google.com/maps/api/js?sensor=false')
content = return_includes(response) content = return_includes(response)
self.assertEqual(content, '') self.assertEqual(content, '')
#regr test for #628 # regr test for #628
response = Response() response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response) content = return_includes(response)
@@ -147,7 +195,7 @@ class testResponse(unittest.TestCase):
def test_cookies(self): def test_cookies(self):
current = setup_clean_session() current = setup_clean_session()
cookie = str(current.response.cookies) 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.assertRegexpMatches(cookie, r'^Set-Cookie: ')
self.assertTrue(session_key in cookie) self.assertTrue(session_key in cookie)
self.assertTrue('Path=/' 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 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 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 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.storage import Storage
from gluon.html import XML_pickle, XML_unpickle from gluon.html import XML_pickle, XML_unpickle
from gluon.html import TAG_pickler, TAG_unpickler from gluon.html import TAG_pickler, TAG_unpickler
from gluon._compat import xrange, PY2, to_native from gluon._compat import xrange, PY2, to_native
from gluon.decoder import decoder
import re
class TestBareHelpers(unittest.TestCase): class TestBareHelpers(unittest.TestCase):
@@ -155,7 +157,7 @@ class TestBareHelpers(unittest.TestCase):
self.assertEqual(rtn, True) self.assertEqual(rtn, True)
# TODO: def test_XmlComponent(self): # TODO: def test_XmlComponent(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for XML.__repr__")
def test_XML(self): def test_XML(self):
# sanitization process # sanitization process
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>').xml(), 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 # seams that __repr__ is no longer enough
##self.assertEqual(XML('1.3'), '1.3') ##self.assertEqual(XML('1.3'), '1.3')
self.assertEqual(XML(u'<div>è</div>').xml(), b'<div>\xc3\xa8</div>') 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() # 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')), len('1.3'))
self.assertEqual(len(XML('1.3').xml()), 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 # you can compare them
##self.assertEqual(XML('a') == XML('a'), True) ##self.assertEqual(XML('a') == XML('a'), True)
# beware that the comparison is made on the XML repr # 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 # bug check for the sanitizer for closing no-close tags
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True), self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True).xml(),
XML('<p>Test</p><br /><p>Test</p><br />')) XML('<p>Test</p><br /><p>Test</p><br />').xml())
# basic flatten test # basic flatten test
self.assertEqual(XML('<p>Test</p>').flatten(), '<p>Test</p>') 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>') 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): def test_XML_pickle_unpickle(self):
# weird test self.assertEqual(str(XML_unpickle(XML_pickle('data to be pickle')[1][0])), 'data to be pickle')
self.assertEqual(XML_unpickle(XML_pickle('data to be pickle')[1][0]), 'data to be pickle')
def test_DIV(self): def test_DIV(self):
# Empty DIV() # 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('<p>Test</p>', _class="class_test").get('_class'), 'class_test')
self.assertEqual(DIV(b'a').xml(), b'<div>a</div>') 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): def test_CAT(self):
# Empty CAT() # Empty CAT()
self.assertEqual(CAT().xml(), b'') 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" # These 2 crash AppVeyor and Travis with: "ImportError: No YAML serializer available"
# self.assertEqual(FORM('<>', _a='1', _b='2').as_yaml(), # 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") # "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(), # TODO check tags content
# '<?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>') self.assertEqual(len(FORM('<>', _a='1', _b='2').as_xml()), 334)
def test_BEAUTIFY(self): def test_BEAUTIFY(self):
#self.assertEqual(BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml(), #self.assertEqual(BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml(),
@@ -670,13 +678,42 @@ class TestBareHelpers(unittest.TestCase):
# TODO: def test_embed64(self): # 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_markdown_serializer(self):
# TODO: def test_markmin_serializer(self): # TODO: def test_markmin_serializer(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for MARKMIN")
def test_MARKMIN(self): def test_MARKMIN(self):
# This test pass with python 2.7 but expected to fail under 2.6 # This test pass with python 2.7 but expected to fail under 2.6
# with self.assertRaises(TypeError) as cm: # 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 # disables prepending the scheme in the return value
def testUnicodeToAsciiUrl(self): def testUnicodeToAsciiUrl(self):
self.assertEquals(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu') self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
self.assertEquals( self.assertEqual(
unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca') unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca')
self.assertRaises(UnicodeError, unicode_to_ascii_authority, self.assertRaises(UnicodeError, unicode_to_ascii_authority,
u'\u4e2d' * 1000) # label is too long u'\u4e2d' * 1000) # label is too long
def testValidUrls(self): def testValidUrls(self):
self.assertEquals(self.x(u'www.Alliancefrancaise.nu'), ( self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), (
'http://www.Alliancefrancaise.nu', None)) '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)) '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)) '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)) ('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.assertEqual(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.assertEqual(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.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.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.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.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.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.assertEquals(self.x( self.assertEqual(self.x(
u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None)) 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)) ('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.assertEqual(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.assertEqual(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.assertEqual(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://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None)) 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)) '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)) 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)) '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)) '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.assertEqual(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'google.ca.'), ('http://google.ca.', None))
self.assertEquals( self.assertEqual(
self.y(u'https://google.ca'), ('https://google.ca', None)) 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)) 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): def testInvalidUrls(self):
self.assertEquals( self.assertEqual(
self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL')) 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')) 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')) 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')) 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.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')) 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')) 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')) 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')) 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')) 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')) 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')) u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL'))
# ############################################################################## # ##############################################################################
+47 -2
View File
@@ -95,14 +95,14 @@ class TestRouter(unittest.TestCase):
""" Test router syntax error """ """ Test router syntax error """
level = logger.getEffectiveLevel() level = logger.getEffectiveLevel()
logger.setLevel(logging.CRITICAL) # disable logging temporarily logger.setLevel(logging.CRITICAL) # disable logging temporarily
self.assertRaises(SyntaxError, load, data='x:y') self.assertRaises(SyntaxError, load, data='x::y')
self.assertRaises( self.assertRaises(
SyntaxError, load, rdict=dict(BASE=dict(badkey="value"))) SyntaxError, load, rdict=dict(BASE=dict(badkey="value")))
self.assertRaises(SyntaxError, load, rdict=dict( self.assertRaises(SyntaxError, load, rdict=dict(
BASE=dict(), app=dict(default_application="name"))) BASE=dict(), app=dict(default_application="name")))
self.myassertRaisesRegex(SyntaxError, "invalid syntax", self.myassertRaisesRegex(SyntaxError, "invalid syntax",
load, data='x:y') load, data='x::y')
self.myassertRaisesRegex(SyntaxError, "unknown key", self.myassertRaisesRegex(SyntaxError, "unknown key",
load, rdict=dict(BASE=dict(badkey="value"))) load, rdict=dict(BASE=dict(badkey="value")))
self.myassertRaisesRegex(SyntaxError, "BASE-only key", self.myassertRaisesRegex(SyntaxError, "BASE-only key",
@@ -873,6 +873,27 @@ class TestRouter(unittest.TestCase):
self.assertEqual(str(URL(a='app2', c='default', f='index', self.assertEqual(str(URL(a='app2', c='default', f='index',
args=['ctr'])), "/app2/index/ctr") args=['ctr'])), "/app2/index/ctr")
# outbound - with extensions
self.assertEqual(
str(URL(a='app', c='default', f='index.json')), "/index.json")
self.assertEqual(
str(URL(a='app', c='default', f='index.json', args=['arg1'])), "/index.json/arg1")
self.assertEqual(str(
URL(a='app', c='default', f='user.json')), "/user.json")
self.assertEqual(str(
URL(a='app', c='default', f='user.json', args=['arg1'])), "/user.json/arg1")
self.assertEqual(str(URL(
a='app', c='default', f='user.json', args=['index'])), "/user.json/index")
self.assertEqual(str(
URL(a='app', c='default', f='index.json', args=['ctr'])), "/index.json/ctr")
self.assertEqual(
str(URL(a='app', c='ctr', f='ctrf1.json', args=['arg'])), "/ctr/ctrf1.json/arg")
self.assertEqual(str(URL(
a='app2', c='default', f='index.json', args=['arg1'])), "/app2/index.json/arg1")
self.assertEqual(str(URL(
a='app2', c='ctr', f='index.json', args=['arg1'])), "/app2/ctr/index.json/arg1")
# inbound # inbound
self.assertEqual( self.assertEqual(
filter_url('http://d.com/arg'), "/app/default/index ['arg']") filter_url('http://d.com/arg'), "/app/default/index ['arg']")
@@ -896,6 +917,30 @@ class TestRouter(unittest.TestCase):
self.assertEqual( self.assertEqual(
filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg") filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg")
# inbound - with extensions
self.assertEqual(
filter_url('http://d.com/index.json'), "/app/default/index.json")
self.assertEqual(filter_url('http://d.com/user.json'), "/app/default/user.json")
self.assertEqual(
filter_url('http://d.com/user.json/arg'), "/app/default/user.json ['arg']")
self.assertEqual(
filter_url('http://d.com/user.json/index'), "/app/default/user.json ['index']")
self.assertEqual(
filter_url('http://d.com/index.json/ctr'), "/app/default/index.json ['ctr']")
self.assertEqual(
filter_url('http://d.com/ctr/ctrf1.json/arg'), "/app/ctr/ctrf1.json ['arg']")
self.assertEqual(filter_url(
'http://d.com/app2/index.json/arg'), "/app2/default/index.json ['arg']")
self.assertEqual(
filter_url('http://d.com/app2/user.json'), "/app2/default/user.json")
self.assertEqual(filter_url(
'http://d.com/app2/user.json/arg'), "/app2/default/user.json ['arg']")
self.assertEqual(filter_url(
'http://d.com/app2/ctr/index.json/arg'), "/app2/ctr/index.json ['arg']")
self.assertEqual(
filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg")
def test_router_functions2(self): def test_router_functions2(self):
''' '''
Test more functions=[something] Test more functions=[something]
+28 -15
View File
@@ -235,6 +235,11 @@ class TestValidators(unittest.TestCase):
self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')]) 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) 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')]) 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 # Test using the set it made for options
vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops') vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')
vldtr.options() vldtr.options()
@@ -434,21 +439,21 @@ class TestValidators(unittest.TestCase):
rtn = IS_NOT_EMPTY()('x') rtn = IS_NOT_EMPTY()('x')
self.assertEqual(rtn, ('x', None)) self.assertEqual(rtn, ('x', None))
rtn = IS_NOT_EMPTY()(' x ') rtn = IS_NOT_EMPTY()(' x ')
self.assertEqual(rtn, ('x', None)) self.assertEqual(rtn, (' x ', None))
rtn = IS_NOT_EMPTY()(None) rtn = IS_NOT_EMPTY()(None)
self.assertEqual(rtn, (None, 'Enter a value')) self.assertEqual(rtn, (None, 'Enter a value'))
rtn = IS_NOT_EMPTY()('') rtn = IS_NOT_EMPTY()('')
self.assertEqual(rtn, ('', 'Enter a value')) self.assertEqual(rtn, ('', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' ') rtn = IS_NOT_EMPTY()(' ')
self.assertEqual(rtn, ('', 'Enter a value')) self.assertEqual(rtn, (' ', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' \n\t') rtn = IS_NOT_EMPTY()(' \n\t')
self.assertEqual(rtn, ('', 'Enter a value')) self.assertEqual(rtn, (' \n\t', 'Enter a value'))
rtn = IS_NOT_EMPTY()([]) rtn = IS_NOT_EMPTY()([])
self.assertEqual(rtn, ([], 'Enter a value')) self.assertEqual(rtn, ([], 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='def')('def') 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') 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') rtn = IS_NOT_EMPTY(empty_regex='def')('abc')
self.assertEqual(rtn, ('abc', None)) self.assertEqual(rtn, ('abc', None))
@@ -531,6 +536,11 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMAIL(error_message='oops')(42) rtn = IS_EMAIL(error_message='oops')(42)
self.assertEqual(rtn, (42, 'oops')) 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): def test_IS_LIST_OF_EMAILS(self):
emails = ['localguy@localhost', '_Yosemite.Sam@example.com'] emails = ['localguy@localhost', '_Yosemite.Sam@example.com']
rtn = IS_LIST_OF_EMAILS()(','.join(emails)) rtn = IS_LIST_OF_EMAILS()(','.join(emails))
@@ -702,15 +712,19 @@ class TestValidators(unittest.TestCase):
def test_IS_LOWER(self): def test_IS_LOWER(self):
rtn = IS_LOWER()('ABC') rtn = IS_LOWER()('ABC')
self.assertEqual(rtn, ('abc', None))
rtn = IS_LOWER()(b'ABC')
self.assertEqual(rtn, (b'abc', None)) self.assertEqual(rtn, (b'abc', None))
rtn = IS_LOWER()('Ñ') rtn = IS_LOWER()('Ñ')
self.assertEqual(rtn, (b'\xc3\xb1', None)) self.assertEqual(rtn, ('ñ', None))
def test_IS_UPPER(self): def test_IS_UPPER(self):
rtn = IS_UPPER()('abc') rtn = IS_UPPER()('abc')
self.assertEqual(rtn, ('ABC', None))
rtn = IS_UPPER()(b'abc')
self.assertEqual(rtn, (b'ABC', None)) self.assertEqual(rtn, (b'ABC', None))
rtn = IS_UPPER()('ñ') rtn = IS_UPPER()('ñ')
self.assertEqual(rtn, (b'\xc3\x91', None)) self.assertEqual(rtn, ('Ñ', None))
def test_IS_SLUG(self): def test_IS_SLUG(self):
rtn = IS_SLUG()('abc123') rtn = IS_SLUG()('abc123')
@@ -780,7 +794,7 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMPTY_OR(IS_EMAIL())('abc') 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_EMAIL())(' abc ') 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) 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')]) 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() rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options()
@@ -843,7 +857,7 @@ class TestValidators(unittest.TestCase):
'|'.join(['Minimum length is 8', '|'.join(['Minimum length is 8',
'Maximum length is 4', 'Maximum length is 4',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'Must include at least 1 upper case', 'Must include at least 1 uppercase',
'Must include at least 1 number'])) 'Must include at least 1 number']))
) )
rtn = IS_STRONG(es=True)('abcde') rtn = IS_STRONG(es=True)('abcde')
@@ -851,7 +865,7 @@ class TestValidators(unittest.TestCase):
('abcde', ('abcde',
'|'.join(['Minimum length is 8', '|'.join(['Minimum length is 8',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'Must include at least 1 upper case', 'Must include at least 1 uppercase',
'Must include at least 1 number'])) 'Must include at least 1 number']))
) )
rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1') rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1')
@@ -859,8 +873,8 @@ class TestValidators(unittest.TestCase):
('Abcde1', ('Abcde1',
'|'.join(['Minimum length is 8', '|'.join(['Minimum length is 8',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'May not include any upper case letters', 'May not include any uppercase letters',
'May not include any lower case letters', 'May not include any lowercase letters',
'May not include any numbers'])) 'May not include any numbers']))
) )
@@ -1029,12 +1043,11 @@ this is the content of the fake file
self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address')) self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address'))
rtn = IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af') rtn = IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', None)) self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', None))
# TODO:
# with py3.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private # with py3.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private
# with py2.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private # with py2.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private
# with gluon.contrib.ipaddr(both current and trunk) is not considered private # with gluon.contrib.ipaddr(both current and trunk) is not considered private
# rtn = IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af') rtn = IS_IPV6(is_routeable=False)('2001::126c:8ffa:fe22:b3af')
# self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None)) self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None))
rtn = IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af') rtn = IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address')) self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address'))
rtn = IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af') rtn = IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af')
+1 -1
View File
@@ -110,7 +110,7 @@ class TestWeb(LiveTest):
self.assertTrue('Welcome Homer' in client.text) self.assertTrue('Welcome Homer' in client.text)
client = WebClient('http://127.0.0.1:8000/admin/default/') 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('site')
client.get('design/welcome') client.get('design/welcome')
+356 -892
View File
File diff suppressed because it is too large Load Diff
+20 -18
View File
@@ -17,6 +17,7 @@ import random
import inspect import inspect
import time import time
import os import os
import sys
import re import re
import logging import logging
import socket import socket
@@ -155,12 +156,12 @@ def get_digest(value):
raise ValueError("Invalid digest algorithm: %s" % value) raise ValueError("Invalid digest algorithm: %s" % value)
DIGEST_ALG_BY_SIZE = { DIGEST_ALG_BY_SIZE = {
128 / 4: 'md5', 128 // 4: 'md5',
160 / 4: 'sha1', 160 // 4: 'sha1',
224 / 4: 'sha224', 224 // 4: 'sha224',
256 / 4: 'sha256', 256 // 4: 'sha256',
384 / 4: 'sha384', 384 // 4: 'sha384',
512 / 4: 'sha512', 512 // 4: 'sha512',
} }
@@ -299,19 +300,20 @@ def initialize_urandom():
try: try:
os.urandom(1) os.urandom(1)
have_urandom = True have_urandom = True
try: if sys.platform != 'win32':
# try to add process-specific entropy
frandom = open('/dev/urandom', 'wb')
try: try:
if PY2: # try to add process-specific entropy
frandom.write(''.join(chr(t) for t in ctokens)) frandom = open('/dev/urandom', 'wb')
else: try:
frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) if PY2:
finally: frandom.write(''.join(chr(t) for t in ctokens))
frandom.close() else:
except IOError: frandom.write(bytes([]).join(bytes([t]) for t in ctokens))
# works anyway finally:
pass frandom.close()
except IOError:
# works anyway
pass
except NotImplementedError: except NotImplementedError:
have_urandom = False have_urandom = False
logger.warning( logger.warning(
+143 -123
View File
@@ -10,7 +10,6 @@
Validators Validators
----------- -----------
""" """
import os import os
import re import re
import datetime import datetime
@@ -21,7 +20,9 @@ import urllib
import struct import struct
import decimal import decimal
import unicodedata 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 gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
from pydal.objects import Field, FieldVirtual, FieldMethod from pydal.objects import Field, FieldVirtual, FieldMethod
from functools import reduce from functools import reduce
@@ -195,7 +196,7 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode or (not(PY2)) self.is_unicode = is_unicode or (not(PY2))
def __call__(self, value): def __call__(self, value):
if not(PY2): # PY3 convert bytes to unicode if not(PY2): # PY3 convert bytes to unicode
value = to_unicode(value) value = to_unicode(value)
if self.is_unicode or not(PY2): if self.is_unicode or not(PY2):
@@ -270,7 +271,7 @@ class IS_EXPR(Validator):
return (value, self.expression(value)) return (value, self.expression(value))
# for backward compatibility # for backward compatibility
self.environment.update(value=value) self.environment.update(value=value)
exec ('__ret__=' + self.expression, self.environment) exec('__ret__=' + self.expression, self.environment)
if self.environment['__ret__']: if self.environment['__ret__']:
return (value, None) return (value, None)
return (value, translate(self.error_message)) return (value, translate(self.error_message))
@@ -452,10 +453,10 @@ class IS_IN_SET(Validator):
if not self.labels: if not self.labels:
items = [(k, k) for (i, k) in enumerate(self.theset)] items = [(k, k) for (i, k) in enumerate(self.theset)]
else: else:
items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] items = [(k, list(self.labels)[i]) for (i, k) in enumerate(self.theset)]
if self.sort: if self.sort:
items.sort(key=lambda o: str(o[1]).upper()) items.sort(key=lambda o: str(o[1]).upper())
if zero and not self.zero is None and not self.multiple: if zero and self.zero is not None and not self.multiple:
items.insert(0, ('', self.zero)) items.insert(0, ('', self.zero))
return items return items
@@ -659,7 +660,7 @@ class IS_IN_DB(Validator):
return (values, None) return (values, None)
else: else:
if field.type in ('id', 'integer'): 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) value = int(value)
elif self.auto_add: elif self.auto_add:
value = self.maybe_add(table, self.fieldnames[0], value) value = self.maybe_add(table, self.fieldnames[0], value)
@@ -823,7 +824,7 @@ class IS_INT_IN_RANGE(Validator):
def str2dec(number): def str2dec(number):
s = str(number) s = str(number)
if not '.' in s: if '.' not in s:
s += '.00' s += '.00'
else: else:
s += '0' * (2 - len(s.split('.')[1])) s += '0' * (2 - len(s.split('.')[1]))
@@ -989,14 +990,15 @@ class IS_DECIMAL_IN_RANGE(Validator):
def is_empty(value, empty_regex=None): def is_empty(value, empty_regex=None):
_value = value
"""test empty field""" """test empty field"""
if isinstance(value, (str, unicodeT)): if isinstance(value, (str, unicodeT)):
value = value.strip() value = value.strip()
if empty_regex is not None and empty_regex.match(value): if empty_regex is not None and empty_regex.match(value):
value = '' value = ''
if value is None or value == '' or value == []: if value is None or value == '' or value == []:
return (value, True) return (_value, True)
return (value, False) return (_value, False)
class IS_NOT_EMPTY(Validator): class IS_NOT_EMPTY(Validator):
@@ -1156,15 +1158,16 @@ class IS_EMAIL(Validator):
""" """
regex = re.compile(''' body_regex = re.compile('''
^(?!\.) # name may not begin with a dot ^(?!\.) # name may not begin with a dot
( (
[-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot [-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot
| |
(?<!\.)\. # single dots only (?<!\.)\. # 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 localhost
| |
@@ -1196,14 +1199,27 @@ class IS_EMAIL(Validator):
self.error_message = error_message self.error_message = error_message
def __call__(self, value): 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: try:
match = self.regex.match(value) match_body = self.body_regex.match(body)
except TypeError: 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 is not None) and (match_domain is not None)
except (TypeError, UnicodeError):
# Value may not be a string where we can look for matches. # 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. # Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date.
match = None match = None
if match: if match:
domain = value.split('@')[1]
if (not self.banned or not self.banned.match(domain)) \ if (not self.banned or not self.banned.match(domain)) \
and (not self.forced or self.forced.match(domain)): and (not self.forced or self.forced.match(domain)):
return (value, None) return (value, None)
@@ -1232,7 +1248,7 @@ class IS_LIST_OF_EMAILS(object):
f = IS_EMAIL() f = IS_EMAIL()
for email in self.split_emails.findall(value): for email in self.split_emails.findall(value):
error = f(email)[1] error = f(email)[1]
if error and not email in bad_emails: if error and email not in bad_emails:
bad_emails.append(email) bad_emails.append(email)
if not bad_emails: if not bad_emails:
return (value, None) return (value, None)
@@ -1372,19 +1388,6 @@ unofficial_url_schemes = [
all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
http_schemes = [None, 'http', 'https'] 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 # Defined in RFC 3490, Section 3.1, Requirement #1
# Use this regex to split the authority component of a unicode URL into # Use this regex to split the authority component of a unicode URL into
# its component labels # its component labels
@@ -1454,18 +1457,15 @@ def unicode_to_ascii_authority(authority):
# We use the ToASCII operation because we are about to put the authority # We use the ToASCII operation because we are about to put the authority
# into an IDN-unaware slot # into an IDN-unaware slot
asciiLabels = [] asciiLabels = []
try: import encodings.idna
import encodings.idna for label in labels:
for label in labels: if label:
if label: asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
asciiLabels.append(to_native(encodings.idna.ToASCII(label))) else:
else: # encodings.idna.ToASCII does not accept an empty string, but
# encodings.idna.ToASCII does not accept an empty string, but # it is necessary for us to allow for empty labels so that we
# it is necessary for us to allow for empty labels so that we # don't modify the URL
# don't modify the URL asciiLabels.append('')
asciiLabels.append('')
except:
asciiLabels = [str(label) for label in labels]
# RFC 3490, Section 4, Step 5 # RFC 3490, Section 4, Step 5
return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels)) return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
@@ -1504,33 +1504,39 @@ def unicode_to_ascii_url(url, prepend_scheme):
""" """
# convert the authority component of the URL into an ASCII punycode string, # convert the authority component of the URL into an ASCII punycode string,
# but encode the rest using the regular URI character encoding # but encode the rest using the regular URI character encoding
components = urlparse.urlparse(url)
groups = url_split_regex.match(url).groups() prepended = False
# If no authority was found # If no authority was found
if not groups[3]: if not components.netloc:
# Try appending a scheme to see if that fixes the problem # Try appending a scheme to see if that fixes the problem
scheme_to_prepend = prepend_scheme or 'http' scheme_to_prepend = prepend_scheme or 'http'
groups = url_split_regex.match( components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url)
to_unicode(scheme_to_prepend) + u'://' + url).groups() prepended = True
# if we still can't find the authority # if we still can't find the authority
if not groups[3]: if not components.netloc:
raise Exception('No authority component found, ' + raise Exception('No authority component found, ' +
'could not decode unicode to US-ASCII') 'could not decode unicode to US-ASCII')
# We're here if we found an authority, let's rebuild the URL # We're here if we found an authority, let's rebuild the URL
scheme = groups[1] scheme = components.scheme
authority = groups[3] authority = components.netloc
path = groups[4] or '' path = components.path
query = groups[5] or '' query = components.query
fragment = groups[7] or '' fragment = components.fragment
if prepend_scheme: if prepended:
scheme = str(scheme) + '://'
else:
scheme = '' scheme = ''
return scheme + unicode_to_ascii_authority(authority) +\ unparsed = urlparse.urlunparse((scheme,
escape_unicode(path) + escape_unicode(query) + str(fragment) 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): class IS_GENERIC_URL(Validator):
@@ -1591,7 +1597,8 @@ class IS_GENERIC_URL(Validator):
% (self.prepend_scheme, self.allowed_schemes)) % (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 = 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): def __call__(self, value):
""" """
@@ -1603,41 +1610,49 @@ class IS_GENERIC_URL(Validator):
prepended with prepend_scheme), and tuple[1] is either prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message None (success!) or the string error_message
""" """
try:
# if the URL does not misuse the '%' character # if we dont have anything or the URL misuses the '%' character
if not self.GENERIC_URL.search(value):
# if the URL is only composed of valid characters if not value or self.GENERIC_URL.search(value):
if self.GENERIC_URL_VALID.match(value): return (value, translate(self.error_message))
# Then split up the URL into its components and check on
# the scheme if '#' in value:
scheme = url_split_regex.match(value).group(2) url, fragment_part = value.split('#')
# Clean up the scheme before we check it else:
if not scheme is None: url, fragment_part = value, ''
scheme = urllib_unquote(scheme).lower() # if the URL is only composed of valid characters
# If the scheme really exists if self.GENERIC_URL_VALID.match(url) and (not fragment_part or self.URL_FRAGMENT_VALID.match(fragment_part)):
if scheme in self.allowed_schemes: # Then parse the URL into its components and check on
# Then the URL is valid try:
return (value, None) components = urlparse.urlparse(urllib_unquote(value))._asdict()
else: except ValueError:
# else, for the possible case of abbreviated URLs with return (value, translate(self.error_message))
# ports, check to see if adding a valid scheme fixes
# the problem (but only do this if it doesn't have # Clean up the scheme before we check it
# one already!) scheme = components['scheme']
if value.find('://') < 0 and None in self.allowed_schemes: if len(scheme) == 0:
schemeToUse = self.prepend_scheme or 'http' scheme = None
prependTest = self.__call__( else:
schemeToUse + '://' + value) scheme = components['scheme'].lower()
# if the prepend test succeeded # If the scheme doesn't really exists
if prependTest[1] is None: if scheme not in self.allowed_schemes or not scheme and ':' in components['path']:
# if prepending in the output is enabled # for the possible case of abbreviated URLs with
if self.prepend_scheme: # ports, check to see if adding a valid scheme fixes
return prependTest # the problem (but only do this if it doesn't have
else: # one already!)
# else return the original, if '://' not in value and None in self.allowed_schemes:
# non-prepended value schemeToUse = self.prepend_scheme or 'http'
return (value, None) prependTest = self.__call__(
except: schemeToUse + '://' + value)
pass # 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 # else the URL is not valid
return (value, translate(self.error_message)) return (value, translate(self.error_message))
@@ -1904,15 +1919,14 @@ class IS_HTTP_URL(Validator):
(possible prepended with prepend_scheme), and tuple[1] is either (possible prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message None (success!) or the string error_message
""" """
try: try:
# if the URL passes generic validation # if the URL passes generic validation
x = IS_GENERIC_URL(error_message=self.error_message, x = IS_GENERIC_URL(error_message=self.error_message,
allowed_schemes=self.allowed_schemes, allowed_schemes=self.allowed_schemes,
prepend_scheme=self.prepend_scheme) prepend_scheme=self.prepend_scheme)
if x(value)[1] is None: if x(value)[1] is None:
componentsMatch = url_split_regex.match(value) components = urlparse.urlparse(value)
authority = componentsMatch.group(4) authority = components.netloc
# if there is an authority component # if there is an authority component
if authority: if authority:
# if authority is a valid IP address # if authority is a valid IP address
@@ -1932,7 +1946,7 @@ class IS_HTTP_URL(Validator):
else: else:
# else this is a relative/abbreviated URL, which will parse # else this is a relative/abbreviated URL, which will parse
# into the URL's path component # 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 # relative case: if this is a valid path (if it starts with
# a slash) # a slash)
if path.startswith('/'): if path.startswith('/'):
@@ -1941,7 +1955,7 @@ class IS_HTTP_URL(Validator):
else: else:
# abbreviated case: if we haven't already, prepend a # abbreviated case: if we haven't already, prepend a
# scheme and see if it fixes the problem # 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' schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(schemeToUse prependTest = self.__call__(schemeToUse
+ '://' + value) + '://' + value)
@@ -2108,7 +2122,6 @@ class IS_URL(Validator):
# If we are not able to convert the unicode url into a # If we are not able to convert the unicode url into a
# US-ASCII URL, then the URL is not valid # US-ASCII URL, then the URL is not valid
return (value, translate(self.error_message)) return (value, translate(self.error_message))
methodResult = subMethod(asciiValue) methodResult = subMethod(asciiValue)
# if the validation of the US-ASCII version of the value failed # if the validation of the US-ASCII version of the value failed
if not methodResult[1] is None: if not methodResult[1] is None:
@@ -2470,7 +2483,7 @@ class IS_LIST_OF(Validator):
class IS_LOWER(Validator): class IS_LOWER(Validator):
""" """
Converts to lower case:: Converts to lowercase::
>>> IS_LOWER()('ABC') >>> IS_LOWER()('ABC')
('abc', None) ('abc', None)
@@ -2480,12 +2493,18 @@ class IS_LOWER(Validator):
""" """
def __call__(self, value): 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): class IS_UPPER(Validator):
""" """
Converts to upper case:: Converts to uppercase::
>>> IS_UPPER()('abc') >>> IS_UPPER()('abc')
('ABC', None) ('ABC', None)
@@ -2495,7 +2514,13 @@ class IS_UPPER(Validator):
""" """
def __call__(self, value): 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): def urlify(s, maxlen=80, keep_underscores=False):
@@ -2602,7 +2627,7 @@ class ANY_OF(Validator):
def __call__(self, value): def __call__(self, value):
for validator in self.subs: for validator in self.subs:
value, error = validator(value) value, error = validator(value)
if error == None: if error is None:
break break
return value, error return value, error
@@ -2742,7 +2767,7 @@ class LazyCrypt(object):
else: else:
digest_alg, key = self.crypt.digest_alg, '' digest_alg, key = self.crypt.digest_alg, ''
if self.crypt.salt: if self.crypt.salt:
if self.crypt.salt == True: if self.crypt.salt is True:
salt = str(web2py_uuid()).replace('-', '')[-16:] salt = str(web2py_uuid()).replace('-', '')[-16:]
else: else:
salt = self.crypt.salt salt = self.crypt.salt
@@ -2827,7 +2852,7 @@ class CRYPT(object):
Supports standard algorithms Supports standard algorithms
>>> for alg in ('md5','sha1','sha256','sha384','sha512'): >>> for alg in ('md5','sha1','sha256','sha384','sha512'):
... print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) ... print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
md5$...$... md5$...$...
sha1$...$... sha1$...$...
sha256$...$... sha256$...$...
@@ -2839,13 +2864,13 @@ class CRYPT(object):
Supports for pbkdf2 Supports for pbkdf2
>>> alg = 'pbkdf2(1000,20,sha512)' >>> alg = 'pbkdf2(1000,20,sha512)'
>>> print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) >>> print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
pbkdf2(1000,20,sha512)$...$... pbkdf2(1000,20,sha512)$...$...
An optional hmac_key can be specified and it is used as salt prefix An optional hmac_key can be specified and it is used as salt prefix
>>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0]) >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0])
>>> print a >>> print(a)
md5$...$... md5$...$...
Even if the algorithm changes the hash can still be validated Even if the algorithm changes the hash can still be validated
@@ -3025,22 +3050,22 @@ class IS_STRONG(object):
all_upper = re.findall("[A-Z]", value) all_upper = re.findall("[A-Z]", value)
if self.upper > 0: if self.upper > 0:
if not len(all_upper) >= self.upper: if not len(all_upper) >= self.upper:
failures.append(translate("Must include at least %s upper case") failures.append(translate("Must include at least %s uppercase")
% str(self.upper)) % str(self.upper))
else: else:
if len(all_upper) > 0: if len(all_upper) > 0:
failures.append( failures.append(
translate("May not include any upper case letters")) translate("May not include any uppercase letters"))
if isinstance(self.lower, int): if isinstance(self.lower, int):
all_lower = re.findall("[a-z]", value) all_lower = re.findall("[a-z]", value)
if self.lower > 0: if self.lower > 0:
if not len(all_lower) >= self.lower: if not len(all_lower) >= self.lower:
failures.append(translate("Must include at least %s lower case") failures.append(translate("Must include at least %s lowercase")
% str(self.lower)) % str(self.lower))
else: else:
if len(all_lower) > 0: if len(all_lower) > 0:
failures.append( failures.append(
translate("May not include any lower case letters")) translate("May not include any lowercase letters"))
if isinstance(self.number, int): if isinstance(self.number, int):
all_number = re.findall("[0-9]", value) all_number = re.findall("[0-9]", value)
if self.number > 0: if self.number > 0:
@@ -3505,10 +3530,7 @@ class IS_IPV6(Validator):
self.error_message = error_message self.error_message = error_message
def __call__(self, value): def __call__(self, value):
try: from gluon._compat import ipaddress
import ipaddress
except ImportError:
from gluon.contrib import ipaddr as ipaddress
try: try:
ip = ipaddress.IPv6Address(to_unicode(value)) ip = ipaddress.IPv6Address(to_unicode(value))
@@ -3732,12 +3754,10 @@ class IS_IPADDRESS(Validator):
self.error_message = error_message self.error_message = error_message
def __call__(self, value): def __call__(self, value):
try: from gluon._compat import ipaddress
from ipaddress import ip_address as IPAddress IPAddress = ipaddress.ip_address
from ipaddress import IPv6Address, IPv4Address IPv6Address = ipaddress.IPv6Address
except ImportError: IPv4Address = ipaddress.IPv4Address
from gluon.contrib.ipaddr import (IPAddress, IPv4Address,
IPv6Address)
try: try:
ip = IPAddress(to_unicode(value)) ip = IPAddress(to_unicode(value))
+51 -40
View File
@@ -13,7 +13,7 @@ from __future__ import print_function
import datetime import datetime
import sys import sys
from gluon._compat import StringIO, thread, xrange from gluon._compat import StringIO, thread, xrange, PY2
import time import time
import threading import threading
import os import os
@@ -134,24 +134,29 @@ class web2pyDialog(object):
def __init__(self, root, options): def __init__(self, root, options):
""" web2pyDialog constructor """ """ web2pyDialog constructor """
import Tkinter if PY2:
import tkMessageBox import Tkinter as tkinter
import tkMessageBox as messagebox
else:
import tkinter
from tkinter import messagebox
bg_color = 'white' bg_color = 'white'
root.withdraw() 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.resizable(0, 0)
self.root.title(ProgramName) self.root.title(ProgramName)
self.options = options self.options = options
self.scheduler_processes = {} self.scheduler_processes = {}
self.menu = Tkinter.Menu(self.root) self.menu = tkinter.Menu(self.root)
servermenu = Tkinter.Menu(self.menu, tearoff=0) servermenu = tkinter.Menu(self.menu, tearoff=0)
httplog = os.path.join(self.options.folder, self.options.log_filename) httplog = os.path.join(self.options.folder, self.options.log_filename)
iconphoto = os.path.join('extras', 'icons', 'web2py.gif') iconphoto = os.path.join('extras', 'icons', 'web2py.gif')
if os.path.exists(iconphoto): 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) self.root.tk.call('wm', 'iconphoto', self.root._w, img)
# Building the Menu # Building the Menu
item = lambda: start_browser(httplog) item = lambda: start_browser(httplog)
@@ -163,16 +168,16 @@ class web2pyDialog(object):
self.menu.add_cascade(label='Server', menu=servermenu) 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) self.menu.add_cascade(label='Pages', menu=self.pagesmenu)
#scheduler menu #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) self.menu.add_cascade(label='Scheduler', menu=self.schedmenu)
#start and register schedulers from options #start and register schedulers from options
self.update_schedulers(start=True) self.update_schedulers(start=True)
helpmenu = Tkinter.Menu(self.menu, tearoff=0) helpmenu = tkinter.Menu(self.menu, tearoff=0)
# Home Page # Home Page
item = lambda: start_browser('http://www.web2py.com/') item = lambda: start_browser('http://www.web2py.com/')
@@ -180,7 +185,7 @@ class web2pyDialog(object):
command=item) command=item)
# About # About
item = lambda: tkMessageBox.showinfo('About web2py', ProgramInfo) item = lambda: messagebox.showinfo('About web2py', ProgramInfo)
helpmenu.add_command(label='About', helpmenu.add_command(label='About',
command=item) command=item)
@@ -194,10 +199,10 @@ class web2pyDialog(object):
else: else:
self.root.protocol('WM_DELETE_WINDOW', self.quit) self.root.protocol('WM_DELETE_WINDOW', self.quit)
sticky = Tkinter.NW sticky = tkinter.NW
# Prepare the logo area # Prepare the logo area
self.logoarea = Tkinter.Canvas(self.root, self.logoarea = tkinter.Canvas(self.root,
background=bg_color, background=bg_color,
width=300, width=300,
height=300) height=300)
@@ -206,22 +211,22 @@ class web2pyDialog(object):
logo = os.path.join('extras', 'icons', 'splashlogo.gif') logo = os.path.join('extras', 'icons', 'splashlogo.gif')
if os.path.exists(logo): if os.path.exists(logo):
img = Tkinter.PhotoImage(file=logo) img = tkinter.PhotoImage(file=logo)
pnl = Tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0) pnl = tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0)
pnl.pack(side='top', fill='both', expand='yes') pnl.pack(side='top', fill='both', expand='yes')
# Prevent garbage collection of img # Prevent garbage collection of img
pnl.image = img pnl.image = img
# Prepare the banner area # Prepare the banner area
self.bannerarea = Tkinter.Canvas(self.root, self.bannerarea = tkinter.Canvas(self.root,
bg=bg_color, bg=bg_color,
width=300, width=300,
height=300) height=300)
self.bannerarea.grid(row=1, column=1, columnspan=2, sticky=sticky) 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), text=str(ProgramVersion + "\n" + ProgramAuthor),
font=('Helvetica', 11), justify=Tkinter.CENTER, font=('Helvetica', 11), justify=tkinter.CENTER,
foreground='#195866', background=bg_color, foreground='#195866', background=bg_color,
height=3).pack(side='top', height=3).pack(side='top',
fill='both', fill='both',
@@ -230,24 +235,24 @@ class web2pyDialog(object):
self.bannerarea.after(1000, self.update_canvas) self.bannerarea.after(1000, self.update_canvas)
# IP # IP
Tkinter.Label(self.root, tkinter.Label(self.root,
text='Server IP:', bg=bg_color, text='Server IP:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=4, justify=tkinter.RIGHT).grid(row=4,
column=1, column=1,
sticky=sticky) sticky=sticky)
self.ips = {} self.ips = {}
self.selected_ip = Tkinter.StringVar() self.selected_ip = tkinter.StringVar()
row = 4 row = 4
ips = [('127.0.0.1', 'Local (IPv4)')] + \ ips = [('127.0.0.1', 'Local (IPv4)')] + \
([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \ ([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \
[(ip, 'Public') for ip in options.ips] + \ [(ip, 'Public') for ip in options.ips] + \
[('0.0.0.0', 'Public')] [('0.0.0.0', 'Public')]
for ip, legend in ips: for ip, legend in ips:
self.ips[ip] = Tkinter.Radiobutton( self.ips[ip] = tkinter.Radiobutton(
self.root, bg=bg_color, highlightthickness=0, self.root, bg=bg_color, highlightthickness=0,
selectcolor='light grey', width=30, selectcolor='light grey', width=30,
anchor=Tkinter.W, text='%s (%s)' % (legend, ip), anchor=tkinter.W, text='%s (%s)' % (legend, ip),
justify=Tkinter.LEFT, justify=tkinter.LEFT,
variable=self.selected_ip, value=ip) variable=self.selected_ip, value=ip)
self.ips[ip].grid(row=row, column=2, sticky=sticky) self.ips[ip].grid(row=row, column=2, sticky=sticky)
if row == 4: if row == 4:
@@ -256,30 +261,30 @@ class web2pyDialog(object):
shift = row shift = row
# Port # Port
Tkinter.Label(self.root, tkinter.Label(self.root,
text='Server Port:', bg=bg_color, text='Server Port:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift, justify=tkinter.RIGHT).grid(row=shift,
column=1, pady=10, column=1, pady=10,
sticky=sticky) sticky=sticky)
self.port_number = Tkinter.Entry(self.root) self.port_number = tkinter.Entry(self.root)
self.port_number.insert(Tkinter.END, self.options.port) self.port_number.insert(tkinter.END, self.options.port)
self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10) self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10)
# Password # Password
Tkinter.Label(self.root, tkinter.Label(self.root,
text='Choose Password:', bg=bg_color, text='Choose Password:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift + 1, justify=tkinter.RIGHT).grid(row=shift + 1,
column=1, column=1,
sticky=sticky) 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.bind('<Return>', lambda e: self.start())
self.password.focus_force() self.password.focus_force()
self.password.grid(row=shift + 1, column=2, sticky=sticky) self.password.grid(row=shift + 1, column=2, sticky=sticky)
# Prepare the canvas # Prepare the canvas
self.canvas = Tkinter.Canvas(self.root, self.canvas = tkinter.Canvas(self.root,
width=400, width=400,
height=100, height=100,
bg='black') bg='black')
@@ -288,19 +293,19 @@ class web2pyDialog(object):
self.canvas.after(1000, self.update_canvas) self.canvas.after(1000, self.update_canvas)
# Prepare the frame # Prepare the frame
frame = Tkinter.Frame(self.root) frame = tkinter.Frame(self.root)
frame.grid(row=shift + 3, column=1, columnspan=2, pady=5, frame.grid(row=shift + 3, column=1, columnspan=2, pady=5,
sticky=sticky) sticky=sticky)
# Start button # Start button
self.button_start = Tkinter.Button(frame, self.button_start = tkinter.Button(frame,
text='start server', text='start server',
command=self.start) command=self.start)
self.button_start.grid(row=0, column=0, sticky=sticky) self.button_start.grid(row=0, column=0, sticky=sticky)
# Stop button # Stop button
self.button_stop = Tkinter.Button(frame, self.button_stop = tkinter.Button(frame,
text='stop server', text='stop server',
command=self.stop) command=self.stop)
@@ -454,9 +459,12 @@ class web2pyDialog(object):
def error(self, message): def error(self, message):
""" Shows error message """ """ Shows error message """
if PY2:
import tkMessageBox import tkMessageBox as messagebox
tkMessageBox.showerror('web2py start server', message) else:
from tkinter import messagebox
messagebox.showerror('web2py start server', message)
def start(self): def start(self):
""" Starts web2py server """ """ Starts web2py server """
@@ -1191,10 +1199,13 @@ def start(cron=True):
if not options.nogui and options.password == '<ask>': if not options.nogui and options.password == '<ask>':
try: try:
import Tkinter if PY2:
import Tkinter as tkinter
else:
import tkinter
havetk = True havetk = True
try: try:
root = Tkinter.Tk() root = tkinter.Tk()
except: except:
pass pass
except (ImportError, OSError): except (ImportError, OSError):
+2 -1
View File
@@ -15,6 +15,7 @@ except ImportError:
import readline import readline
try: try:
from gluon import DAL from gluon import DAL
from gluon.fileutils import open_file
except ImportError as err: except ImportError as err:
print('gluon path not found') print('gluon path not found')
@@ -531,7 +532,7 @@ class setCopyDB():
self.db.export_to_csv_file(open('tmp.sql', 'wb')) self.db.export_to_csv_file(open('tmp.sql', 'wb'))
print 'importing data...' 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() other_db.commit()
print 'done!' print 'done!'
print 'Attention: do not run this program again or you end up with duplicate records' print 'Attention: do not run this program again or you end up with duplicate records'
+13 -7
View File
@@ -18,7 +18,7 @@ legacy_db(legacy_db.mytable.id>0).select()
If the script crashes this is might be due to that fact that the data_type_map dictionary below is incomplete. If the script crashes this is might be due to that fact that the data_type_map dictionary below is incomplete.
Please complete it, improve it and continue. Please complete it, improve it and continue.
Created by Falko Krause, minor modifications by Massimo Di Pierro and Ron McOuat Created by Falko Krause, minor modifications by Massimo Di Pierro, Ron McOuat and Marvi Benedet
''' '''
import subprocess import subprocess
import re import re
@@ -52,10 +52,11 @@ data_type_map = dict(
) )
def mysql(database_name, username, password): def mysql(database_name, username, password, host):
p = subprocess.Popen(['mysql', p = subprocess.Popen(['mysql',
'--user=%s' % username, '--user=%s' % username,
'--password=%s' % password, '--password=%s' % password,
'--host=%s' % host,
'--execute=show tables;', '--execute=show tables;',
database_name], database_name],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@@ -64,14 +65,15 @@ def mysql(database_name, username, password):
sql_showtables, stderr = p.communicate() sql_showtables, stderr = p.communicate()
tables = [re.sub( tables = [re.sub(
'\|\s+([^\|*])\s+.*', '\1', x) for x in sql_showtables.split()[1:]] '\|\s+([^\|*])\s+.*', '\1', x) for x in sql_showtables.split()[1:]]
connection_string = "legacy_db = DAL('mysql://%s:%s@localhost/%s')" % ( connection_string = "legacy_db = DAL('mysql://%s:%s@%s/%s')" % (
username, password, database_name) username, password, host, database_name)
legacy_db_table_web2py_code = [] legacy_db_table_web2py_code = []
for table_name in tables: for table_name in tables:
#get the sql create statement #get the sql create statement
p = subprocess.Popen(['mysqldump', p = subprocess.Popen(['mysqldump',
'--user=%s' % username, '--user=%s' % username,
'--password=%s' % password, '--password=%s' % password,
'--host=%s' % host,
'--skip-add-drop-table', '--skip-add-drop-table',
'--no-data', database_name, '--no-data', database_name,
table_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) table_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -106,9 +108,13 @@ def mysql(database_name, username, password):
legacy_db_table_web2py_code) legacy_db_table_web2py_code)
return legacy_db_web2py_code return legacy_db_web2py_code
regex = re.compile('(.*?):(.*?)@(.*)') regex = re.compile('(.*):(.*)@((.*)/)?(.*)')
if len(sys.argv) < 2 or not regex.match(sys.argv[1]): if len(sys.argv) < 2 or not regex.match(sys.argv[1]):
print 'USAGE:\n\n extract_mysql_models.py username:password@data_basename\n\n' print 'USAGE:\n\n extract_mysql_models.py username:password@[host/]data_basename\n\n'
else: else:
m = regex.match(sys.argv[1]) m = regex.match(sys.argv[1])
print mysql(m.group(3), m.group(1), m.group(2)) username = m.group(1)
password = m.group(2)
host = m.group(4) or 'localhost'
db_name = m.group(5)
print mysql(database_name = db_name, username = username, password = password, host = host)
@@ -216,6 +216,7 @@ fi
/etc/init.d/nginx start /etc/init.d/nginx start
systemctl start emperor.uwsgi.service systemctl start emperor.uwsgi.service
systemctl enable emperor.uwsgi.service
echo <<EOF echo <<EOF
you can stop uwsgi and nginx with you can stop uwsgi and nginx with